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,15 @@
1
+ import assert from 'node:assert/strict'
2
+ import { parseWorkerMessage } from '../../../src/debugger/protocol.js'
3
+
4
+ const event = { event: 'stopped', stop_id: 'stop-1', thread_id: 'thread-1', stops: [] }
5
+ assert.deepEqual(parseWorkerMessage(event), { kind: 'event', value: event })
6
+ assert.deepEqual(parseWorkerMessage({ id: 1, result: { locals: [] } }), { kind: 'reply', id: 1, result: { locals: [] } })
7
+ assert.deepEqual(parseWorkerMessage({ id: 2, error: { code: 'DEBUGGER_STALE_REFERENCE' } }), {
8
+ kind: 'reply', id: 2, result: {}, error: { code: 'DEBUGGER_STALE_REFERENCE' }
9
+ })
10
+ for (const invalid of [null, [], { event: 42 }, { event: 'stopped', stop_id: 42 },
11
+ { id: '1', result: {} }, { id: -1, result: {} }, { id: 1.2, result: {} },
12
+ { id: 1, result: 'private-debuggee-value' }, { id: 1, error: { code: 42 } }]) {
13
+ assert.throws(() => parseWorkerMessage(invalid), /^Error: INVALID_WORKER_/, 'malformed worker input is rejected without exposing its contents')
14
+ }
15
+ console.log('PASS validated worker events/replies and content-free protocol errors')
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict'
2
2
  import { DebuggerSessions, debuggerExpiry } from '../../../src/debugger/sessions.js'
3
3
  import { DebuggerError, type WorkerEvent } from '../../../src/debugger/worker.js'
4
4
  import type { DebugNative, DebugTarget } from '../../../src/debugger/android.js'
5
- import { debuggerContracts, debuggerTools } from '../../../src/server/debugger-schema.js'
5
+ import { debuggerContracts, debuggerTools } from '../../../src/server/schemas/debugger-schema.js'
6
6
  assert.equal(debuggerExpiry(0, 0, 0, 59999), null)
7
7
  assert.equal(debuggerExpiry(0, 60000, 0, 60000), 'pause_limit', 'polling cannot extend pause expiry')
8
8
  assert.equal(debuggerExpiry(0, 0, undefined, 300000), 'idle_limit')
@@ -0,0 +1,42 @@
1
+ import assert from 'node:assert/strict'
2
+ import { adjustControl } from '../../../src/interact/adjustment/adjustment.js'
3
+ import type { AdjustmentDependencies } from '../../../src/interact/adjustment/adjustment-context.js'
4
+ import type { ActionTargetResolved, DeviceInfo } from '../../../src/types.js'
5
+ import type { UiElement } from '../../../src/interact/model.js'
6
+
7
+ const device: DeviceInfo = { platform: 'android', id: 'fixture', model: 'fixture', osVersion: 'fixture', simulator: true }
8
+ let value = 10
9
+ const calls: string[] = []
10
+ const element = (): UiElement => ({ visible: true, enabled: true, role: 'slider', stable_id: 'duration', bounds: [0, 0, 300, 40],
11
+ state: { value, value_range: { min: 0, max: 100 } }, actions: ['set_value'] })
12
+ const handle = { elementId: 'handle', platform: 'android' as const, deviceId: device.id, bounds: [0, 0, 300, 40] as [number, number, number, number], index: 0, stable_id: 'duration' }
13
+ const target = (): ActionTargetResolved => ({ elementId: 'handle', text: null, resource_id: null, accessibility_id: null, class: 'Slider', bounds: handle.bounds, index: 0 })
14
+ const deps: AdjustmentDependencies = {
15
+ observe: async () => ({ device, elements: [element()] }), fingerprint: async () => 'fingerprint',
16
+ getHandle: () => handle, currentHandle: () => ({ el: element(), index: 0 }), target,
17
+ wait: async () => { throw new Error('handle targeting must not use selector waits') },
18
+ find: async () => { throw new Error('handle targeting must not use semantic search') },
19
+ expectState: async input => {
20
+ calls.push('verify')
21
+ return { success: true, element_id: 'handle', expected_state: { property: input.property, expected: input.expected },
22
+ observed_state: { property: input.property, value }, trace: { action_id: 'assertion', steps: [], final_outcome: 'success', attempts: 1 } }
23
+ },
24
+ service: async () => ({ resolved: { id: device.id }, interact: { setAdjustableValue: async () => { calls.push('semantic'); return { success: false } } } }),
25
+ tap: async () => { calls.push('coordinate'); value = 20; return { success: true, device } },
26
+ swipe: async () => { throw new Error('a converged coordinate adjustment must not dispatch a gesture') }
27
+ }
28
+ const already = await adjustControl({ element_id: 'handle', targetValue: 10 }, deps)
29
+ assert.equal(already.success, true)
30
+ assert.equal(already.attempts, 0)
31
+ assert.deepEqual(calls, [])
32
+ const adjusted = await adjustControl({ element_id: 'handle', targetValue: 20 }, deps)
33
+ assert.equal(adjusted.success, true)
34
+ assert.equal(adjusted.attempts, 2)
35
+ assert.equal(adjusted.adjustment_mode, 'coordinate')
36
+ assert.deepEqual(calls, ['semantic', 'coordinate', 'verify'], 'failed direct capability falls back in the established order')
37
+ calls.length = 0
38
+ const stale = await adjustControl({ element_id: 'missing', targetValue: 20 }, { ...deps, getHandle: () => undefined })
39
+ assert.equal(stale.success, false)
40
+ assert.equal(stale.failure_code, 'STALE_REFERENCE')
41
+ assert.deepEqual(calls, [], 'missing handles cannot dispatch native input')
42
+ console.log('PASS injected adjustment service: read-only convergence, strategy order, attempt accounting and stale-handle rejection')
@@ -0,0 +1,35 @@
1
+ import assert from 'node:assert/strict'
2
+ import type { ExpectStateResponse } from '../../../src/types.js'
3
+ import { verifyAdjustment, type AdjustmentVerificationInput } from '../../../src/interact/adjustment/adjustment-verification.js'
4
+
5
+ const input: AdjustmentVerificationInput = {
6
+ labeled: false, currentEl: { state: { value: 10 } },
7
+ actualState: { property: 'value', value: 10 }, property: 'value', target: 20, tolerance: 0.5,
8
+ expectation: { element_id: 'control', property: 'value', expected: 20, platform: 'android', deviceId: 'fixture' }
9
+ }
10
+ let observations = 0
11
+ let assertions = 0
12
+ const verified = await verifyAdjustment(input, {
13
+ observe: async () => { throw new Error('ordinary verification must use the assertion boundary') },
14
+ expectState: async args => {
15
+ assertions++
16
+ assert.equal(args.element_id, 'control')
17
+ return { success: false, observed_state: { value: 20.25 }, reason: 'exact comparison failed' } as ExpectStateResponse
18
+ }
19
+ })
20
+ assert.equal(assertions, 1)
21
+ assert.equal(verified.withinTolerance, true, 'numeric tolerance is independent of exact assertion success')
22
+ assert.equal(verified.observedState?.value, 20.25)
23
+ const stale = await verifyAdjustment({ ...input, labeled: true, actualState: { property: 'value', value: 20 } }, {
24
+ observe: async () => { observations++; return { elements: [] } },
25
+ expectState: async () => { throw new Error('label verification must acquire a new tree') }
26
+ })
27
+ assert.equal(observations, 1)
28
+ assert.equal(stale.withinTolerance, false, 'fallback state must not establish fresh convergence')
29
+ assert.equal(stale.verification.success, false)
30
+ assert.equal(stale.observedState?.value, 20, 'last readable state remains available for diagnostics')
31
+ await assert.rejects(verifyAdjustment(input, {
32
+ observe: async () => null,
33
+ expectState: async () => { throw new Error('ACTION_TIMEOUT') }
34
+ }), /ACTION_TIMEOUT/, 'timeouts propagate without manufacturing an observation')
35
+ console.log('PASS adjustment verification dependencies, tolerance, freshness and timeout preservation')
@@ -0,0 +1,63 @@
1
+ import assert from 'node:assert/strict'
2
+ import type { Clock } from '../../../src/runtime/clock.js'
3
+ import type { UiTree } from '../../../src/interact/observation.js'
4
+ import { waitForUI } from '../../../src/interact/waits/ui-wait.js'
5
+ import { waitForScreenChange } from '../../../src/interact/waits/screen-wait.js'
6
+ import { findElement } from '../../../src/interact/targeting/find-element.js'
7
+ import { expectState } from '../../../src/interact/assertions/state-assertion.js'
8
+ import { _resolvedTargetFromElement } from '../../../src/interact/targeting/element-helpers.js'
9
+
10
+ function fakeClock() {
11
+ let now = 1000
12
+ const delays: number[] = []
13
+ const clock: Clock = { now: () => now, sleep: async ms => { delays.push(ms); now += ms } }
14
+ return { clock, delays }
15
+ }
16
+ const resolve = (_platform: string, _device: string | undefined, element: NonNullable<UiTree['elements']>[number], index: number) => _resolvedTargetFromElement('test-id', element, index)
17
+ const present = (clock: Clock): UiTree => ({ captured_at_ms: clock.now(), elements: [{ text: 'Button', state: { enabled: true } }] })
18
+
19
+ {
20
+ const { clock, delays } = fakeClock()
21
+ const result = await waitForUI({ selector: { text: 'Button' }, timeout_ms: 500, poll_interval_ms: 50 }, {
22
+ clock, observe: async () => present(clock), resolve
23
+ })
24
+ assert.equal(result?.status, 'success')
25
+ assert.deepEqual(result?.metrics, { latency_ms: 50, poll_count: 2, attempts: 1 })
26
+ assert.deepEqual(delays, [50])
27
+ }
28
+ {
29
+ const { clock, delays } = fakeClock()
30
+ const result = await waitForUI({ selector: { text: 'missing' }, timeout_ms: 100, poll_interval_ms: 50,
31
+ retry: { max_attempts: 2, backoff_ms: 20 } }, { clock, observe: async () => ({ elements: [] }), resolve })
32
+ assert.equal(result?.status, 'timeout')
33
+ assert.deepEqual(result?.metrics, { latency_ms: 320, poll_count: 6, attempts: 2 })
34
+ assert.deepEqual(delays, [50, 50, 50, 20, 50, 50, 50])
35
+ }
36
+ {
37
+ const { clock, delays } = fakeClock()
38
+ let samples = 0
39
+ const result = await waitForScreenChange({ previousFingerprint: 'old', timeoutMs: 500, pollIntervalMs: 50 }, {
40
+ clock, fingerprint: async () => ({ fingerprint: samples++ === 0 ? 'old' : 'new' })
41
+ })
42
+ assert.equal(result.success, true)
43
+ assert.equal(result.elapsedMs, 100)
44
+ assert.deepEqual(delays, [50, 50])
45
+ }
46
+ {
47
+ const { clock, delays } = fakeClock()
48
+ const result = await findElement({ query: 'missing', timeoutMs: 200 }, async () => ({ elements: [] }), clock)
49
+ assert.equal(result.found, false)
50
+ assert.deepEqual(delays, [100, 100, 100])
51
+ }
52
+ {
53
+ const { clock, delays } = fakeClock()
54
+ const result = await expectState({ selector: { text: 'Button' }, property: 'enabled', expected: true }, {
55
+ clock, observe: async () => present(clock), getHandle: () => undefined,
56
+ currentHandle: () => null, target: (platform, device, match) => resolve(platform, device, match.el, match.idx)
57
+ })
58
+ assert.equal(result.success, true)
59
+ assert.equal(result.stabilization_window_ms, 150)
60
+ assert.deepEqual(delays, [150])
61
+ assert(result.trace.steps.every(step => step.timestamp >= 1000 && step.timestamp <= 1150))
62
+ }
63
+ console.log('PASS injected clocks: wait stability, retry/backoff deadlines, screen confirmation, search expiry and state trace timestamps without global timer replacement')
@@ -0,0 +1,19 @@
1
+ import assert from 'node:assert/strict'
2
+ import { _matchesSelector, _resolveActionableAncestor, _normalizeBounds } from '../../../src/interact/targeting/element-helpers.js'
3
+ import { _readNumericControlRange, _quantizeTargetValue } from '../../../src/interact/adjustment/control-values.js'
4
+
5
+ assert(_matchesSelector({ text: 'Save', resourceId: 'app:id/save' }, { text: 'save', resource_id: 'app:id/save' }))
6
+ assert(!_matchesSelector({ text: 'Save', resourceId: 'app:id/other' }, { text: 'save', resource_id: 'app:id/save' }), 'all supplied selector fields constrain selection')
7
+ assert(_matchesSelector({ contentDesc: 'Save changes' }, { accessibility_id: 'changes', contains: true }))
8
+ assert(!_matchesSelector({ text: 'Save' }, undefined))
9
+ assert.deepEqual(_normalizeBounds(['1', 2, 3, 4]), [1, 2, 3, 4])
10
+ assert.equal(_normalizeBounds([1, 'invalid', 3, 4]), null)
11
+ const elements = [
12
+ { id: 'parent', clickable: true, bounds: [0, 0, 100, 100] },
13
+ { parentId: 'parent', text: 'Save', bounds: [20, 20, 40, 40] }
14
+ ]
15
+ assert.deepEqual(_resolveActionableAncestor(elements, { el: elements[1], idx: 1 }), { el: elements[0], idx: 0 })
16
+ assert.deepEqual(_readNumericControlRange({ state: { value: '3 of 10' } }), { min: 0, max: 10 })
17
+ assert.equal(_readNumericControlRange({ state: { value_range: { min: 10, max: 0 } } }), null)
18
+ assert.equal(_quantizeTargetValue(0.26, 0, 1, 0.1), 0.3)
19
+ console.log('PASS injected-free selector, actionable ancestor, numeric range and snapping helpers')
@@ -0,0 +1,17 @@
1
+ import assert from 'node:assert/strict'
2
+ import { waitForUICore } from '../../../src/interact/waits/legacy-wait.js'
3
+ import type { LegacyWaitDependencies } from '../../../src/interact/waits/legacy-wait-state.js'
4
+
5
+ const calls: string[] = []
6
+ const deps: LegacyWaitDependencies = {
7
+ fingerprint: async () => { calls.push('fingerprint'); return { fingerprint: 'initial' } },
8
+ logs: async () => { calls.push('logs'); return { logs: ['before'] } },
9
+ stream: async () => { calls.push('stream'); return { entries: [{ message: 'operation done' }] } },
10
+ find: async () => { throw new Error('log waits must not search UI elements') },
11
+ snapshot: async () => { throw new Error('successful waits must not collect failure snapshots') }
12
+ }
13
+ const result = await waitForUICore({ type: 'log', query: 'done', timeoutMs: 1000 }, deps)
14
+ assert.equal(result.success, true)
15
+ assert.equal(result.observed_state, true)
16
+ assert.deepEqual(calls, ['fingerprint', 'logs', 'stream'])
17
+ console.log('PASS injected legacy wait: baseline collection, stream-first matching and no failure snapshot')
@@ -0,0 +1,20 @@
1
+ import assert from 'node:assert/strict'
2
+ import { compareReadableState } from '../../../src/interact/assertions/state-comparison.js'
3
+
4
+ assert.equal(compareReadableState('checked', true, { checked: true }).success, true)
5
+ const invalidBoolean = compareReadableState('checked', 'true', { checked: true })
6
+ assert.equal(invalidBoolean.success, false)
7
+ assert.equal(invalidBoolean.reason, 'expected checked must be boolean')
8
+ assert.equal(invalidBoolean.observedValue, true)
9
+ assert.equal(invalidBoolean.rawValue, null)
10
+ assert.equal(compareReadableState('value', 20, { value: '20' }).success, false, 'numeric assertions do not coerce readable strings')
11
+ assert.equal(compareReadableState('value', '20', { value: '20' }).success, true)
12
+ assert.equal(compareReadableState('selected', { id: 'dark' }, { selected: { id: 'dark', label: 'Dark' } }).success, true)
13
+ assert.equal(compareReadableState('selected', false, { selected: false }).success, true)
14
+ assert.equal(compareReadableState('selected', true, { selected: 'true' }).reason, 'selected state is not boolean')
15
+ assert.equal(compareReadableState('text_value', '', { text_value: '' }).reason, 'expected text_value must be string')
16
+ assert.equal(compareReadableState('value', 20, null).reason, 'value state unavailable')
17
+ const range = { min: 0, max: 20 }
18
+ assert.equal(compareReadableState('value_range', range, { value_range: range }).success, true, 'fallback comparison preserves reference equality')
19
+ assert.equal(compareReadableState('value_range', { min: 0, max: 20 }, { value_range: range }).success, false)
20
+ console.log('PASS readable-state comparison compatibility without UI or global mocks')
@@ -0,0 +1,35 @@
1
+ import assert from 'node:assert/strict'
2
+ import { ResolvedElements } from '../../../src/interact/targeting/resolved-elements.js'
3
+ import { findScopedElement, resolveUiChangeScope } from '../../../src/interact/targeting/target-scope.js'
4
+ import type { ResolvedUiElementContext } from '../../../src/interact/model.js'
5
+
6
+ const store = new ResolvedElements(2)
7
+ const context = (id: string): ResolvedUiElementContext => ({
8
+ elementId: id, platform: 'android', deviceId: 'fixture', bounds: null, index: 0, stable_id: id
9
+ })
10
+ store.remember('one', context('one'))
11
+ store.remember('two', context('two'))
12
+ store.remember('one', context('one'))
13
+ store.remember('three', context('three'))
14
+ assert.equal(store.get('two'), undefined, 'oldest handle is evicted; refresh updates insertion order')
15
+ assert(store.get('one'))
16
+ const tree = { elements: [{ text: 'Root', stable_id: 'one', children: [1] }, { text: 'Child', parentId: 0 }] }
17
+ const match = findScopedElement(tree, 'one', store)
18
+ assert.equal(match?.index, 0)
19
+ assert.equal(match?.reason, 'stable_id_match')
20
+ let calls = 0
21
+ const subtree = resolveUiChangeScope(tree, 'subtree', 'custom-handle', (_tree, id) => {
22
+ calls++
23
+ assert.equal(id, 'custom-handle')
24
+ return { index: 0, stableId: 'one', reason: 'injected_lookup' }
25
+ })
26
+ assert.equal(calls, 1)
27
+ assert.equal(subtree.elements.length, 2)
28
+ assert.equal(subtree.resolution.reason, 'injected_lookup')
29
+ assert.equal(resolveUiChangeScope(tree, 'subtree', '', () => { throw new Error('must not resolve empty targets') }).error?.code, 'INVALID_SCOPE')
30
+ assert.equal(resolveUiChangeScope(tree, 'subtree', 'missing', () => null).error?.code, 'ELEMENT_NOT_FOUND')
31
+ const other = new ResolvedElements(2)
32
+ assert.equal(other.get('one'), undefined, 'resolution stores do not share process-global state')
33
+ store.clear()
34
+ assert.equal(store.get('one'), undefined)
35
+ console.log('PASS bounded handle retention and injected subtree resolution without static replacements')
@@ -5,7 +5,7 @@ import {
5
5
  classifyStatus,
6
6
  _setTimestampsForTests,
7
7
  ToolsNetwork
8
- } from '../../../src/network/index.js'
8
+ } from '../../../src/observe/network/index.js'
9
9
 
10
10
  function run() {
11
11
 
@@ -0,0 +1,30 @@
1
+ import assert from 'node:assert/strict'
2
+ import sharp from 'sharp'
3
+ import { encodeScreenshot } from '../../../src/observe/screenshot-encoding.js'
4
+
5
+ async function run() {
6
+ const alpha = await sharp({ create: { width: 2, height: 2, channels: 4, background: { r: 255, g: 0, b: 0, alpha: 0.5 } } }).png().toBuffer()
7
+ const opaque = await sharp({ create: { width: 2, height: 2, channels: 3, background: 'red' } }).png().toBuffer()
8
+ for (const platform of ['android', 'ios'] as const) {
9
+ const transparent = await encodeScreenshot(alpha, platform)
10
+ assert.equal(transparent.screenshot_mime, 'image/webp')
11
+ assert.equal((await sharp(Buffer.from(transparent.screenshot, 'base64')).metadata()).hasAlpha, true)
12
+ assert.equal(transparent.screenshot_fallback_mime, platform === 'ios' ? 'image/png' : 'image/jpeg')
13
+ if (platform === 'ios') assert.equal(transparent.screenshot_fallback, alpha.toString('base64'))
14
+ const solid = await encodeScreenshot(opaque, platform)
15
+ assert.equal(solid.screenshot_mime, 'image/webp')
16
+ assert.equal(solid.screenshot_fallback_mime, 'image/jpeg')
17
+ assert.equal((await sharp(Buffer.from(solid.screenshot_fallback!, 'base64')).metadata()).format, 'jpeg')
18
+ }
19
+ // An unreadable capture must survive conversion failures unchanged.
20
+ const invalid = Buffer.from('unreadable screenshot')
21
+ const originalError = console.error
22
+ try {
23
+ console.error = () => {}
24
+ for (const platform of ['android', 'ios'] as const) {
25
+ assert.deepEqual(await encodeScreenshot(invalid, platform), { screenshot: invalid.toString('base64'), screenshot_mime: 'image/png' })
26
+ }
27
+ } finally { console.error = originalError }
28
+ console.log('Screenshot alpha, fallback format and failed conversion tests passed')
29
+ }
30
+ run().catch(error => { console.error(error); process.exit(1) })
@@ -0,0 +1,14 @@
1
+ import assert from 'node:assert/strict'
2
+ import { snapshotLog, streamLog } from '../../../src/observe/snapshot-logs.js'
3
+
4
+ const iso = '2026-10-30T12:00:00Z'
5
+ assert.deepEqual(snapshotLog({ timestamp: 123, message: 'warning', levelName: 'warn' }), { timestamp: 123, message: 'warning', level: 'WARN' })
6
+ assert.equal(streamLog({ timestamp: 123, message: 'warning' }).timestamp, null)
7
+ assert.equal(snapshotLog({ timestamp: 123, _iso: iso, msg: 'hello' }).timestamp, 123)
8
+ assert.equal(streamLog({ timestamp: 123, _iso: iso, msg: 'hello' }).timestamp, Date.parse(iso))
9
+ assert.equal(snapshotLog('FATAL EXCEPTION: crash').level, 'ERROR')
10
+ assert.equal(streamLog('WARN: slow').level, 'WARN')
11
+ assert.equal(snapshotLog({ timestamp: 'invalid', msg: 'hello' }).timestamp, null)
12
+ assert.deepEqual(snapshotLog(null), { timestamp: null, level: 'INFO', message: '' })
13
+ assert.equal(snapshotLog({ context: 'value' }).message, '{"context":"value"}')
14
+ console.log('Snapshot and stream timestamp precedence and log normalization tests passed')
@@ -0,0 +1,60 @@
1
+ import assert from 'node:assert/strict'
2
+ import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
3
+ import { tmpdir } from 'node:os'
4
+ import path from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
6
+ import { architectureGraph } from '../../../scripts/architecture-graph.mjs'
7
+ import { checkArchitecture } from '../../../scripts/check-architecture.mjs'
8
+
9
+ const root = await mkdtemp(path.join(tmpdir(), 'mcp-architecture-'))
10
+ try {
11
+ const files: Record<string, string> = {
12
+ 'package.json': JSON.stringify({ type: 'module' }),
13
+ 'tsconfig.json': JSON.stringify({ compilerOptions: {
14
+ module: 'NodeNext', moduleResolution: 'NodeNext', target: 'ES2022',
15
+ paths: { '@server/*': ['./src/server/*'] }
16
+ }, include: ['src/**/*.ts'] }),
17
+ 'src/server/entry.ts': 'export const value = 1',
18
+ 'src/contracts/bad.ts': "export { value } from '@server/entry.js'",
19
+ 'src/interact/entry.ts': "export const load = () => import('../server/entry.js')",
20
+ 'src/interact/adjustment/control-values.ts': "import { spawn } from 'node:child_process'; export const native = spawn",
21
+ 'src/cycle/a.ts': "export { b } from './b.js'; export const a = 1",
22
+ 'src/cycle/b.ts': "export const b = () => import('./a.js')",
23
+ 'src/type-cycle/a.ts': "import type { B } from './b.js'; export interface A { b: B }",
24
+ 'src/type-cycle/b.ts': "import { type A } from './a.js'; export interface B { a: A }",
25
+ 'src/import-equals.ts': "import entry = require('./server/entry.js'); export { entry }",
26
+ 'src/require.ts': "export const load = () => require('./server/entry.js')"
27
+ }
28
+ for (const [file, code] of Object.entries(files)) {
29
+ const target = path.join(root, file)
30
+ await mkdir(path.dirname(target), { recursive: true })
31
+ await writeFile(target, code)
32
+ }
33
+ const config = path.join(root, 'tsconfig.json')
34
+ const graph = await architectureGraph(config)
35
+ const edge = (source: string, target: string) => graph.find(item => item.source === source && item.target === target)
36
+ assert(edge('src/contracts/bad.ts', 'src/server/entry.ts'), 'Resolve aliased re-exports to .ts sources')
37
+ assert(edge('src/interact/entry.ts', 'src/server/entry.ts'), 'Resolve literal dynamic .js imports')
38
+ assert(edge('src/require.ts', 'src/server/entry.ts'), 'Resolve literal require calls')
39
+ assert(edge('src/interact/adjustment/control-values.ts', 'node:child_process'), 'Include Node builtins')
40
+ assert.deepEqual(edge('src/type-cycle/b.ts', 'src/type-cycle/a.ts')?.importKinds, ['type'])
41
+ assert(edge('src/import-equals.ts', 'src/server/entry.ts'), 'Resolve import-equals require syntax')
42
+ const rejected = spawnSync(process.execPath, ['scripts/check-architecture.mjs', config], { encoding: 'utf8' })
43
+ assert.equal(rejected.status, 1, rejected.stderr)
44
+ assert(rejected.stderr.includes('Production runtime dependencies must be cycle-free'))
45
+ const results = checkArchitecture(graph)
46
+ assert(results.every(result => result.violations.length > 0), 'Every rule must reject its deliberately broken fixture')
47
+ const cycles = JSON.stringify(results.at(-1)?.violations)
48
+ assert(cycles.includes('src/cycle/a.ts'), 'Catch a cycle through a re-export and dynamic import')
49
+ assert(!cycles.includes('src/type-cycle'), 'Type-only cycles are not runtime cycles')
50
+ assert(checkArchitecture([]).every(result => result.violations.length > 0), 'An empty graph must fail closed')
51
+
52
+ await writeFile(path.join(root, 'src/nonliteral.ts'), 'export const load = (name: string) => import(name)')
53
+ await assert.rejects(architectureGraph(config), /Nonliteral dependency/)
54
+ await rm(path.join(root, 'src/nonliteral.ts'))
55
+ await writeFile(path.join(root, 'src/unresolved.ts'), "import './missing.js'")
56
+ await assert.rejects(architectureGraph(config), /Unresolved dependency/)
57
+ console.log('PASS architecture negative controls: NodeNext, aliases, re-exports, dynamic/require imports, pure boundaries, runtime cycles, type-only cycles, empty and unresolved graphs')
58
+ } finally {
59
+ await rm(root, { recursive: true, force: true })
60
+ }
@@ -0,0 +1,58 @@
1
+ import assert from 'node:assert/strict'
2
+ import { measure, compare } from '../../../scripts/code-quality.mjs'
3
+
4
+ const file = 'src/interact/quality-fixture.ts'
5
+ const inspect = (code: string) => measure(file, code)
6
+ const has = (findings: Awaited<ReturnType<typeof inspect>>, rule: string) => findings.some(f => f.rule === rule)
7
+ const baseline = (findings: Awaited<ReturnType<typeof inspect>>) => findings.map(({ line: _line, ...entry }) => ({ ...entry, reason: 'Test legacy exception' }))
8
+
9
+ async function main() {
10
+ assert.deepEqual(await inspect('export const twice = (value: number) => value * 2'), [])
11
+ const hugeClass = await inspect(`export class Huge {\n${Array.from({ length: 301 }, (_, i) => `field${i} = ${i}`).join('\n')}\n}`)
12
+ assert(has(hugeClass, 'class-lines'))
13
+ const hugeFunction = await inspect(`export function work() {\n${Array.from({ length: 81 }, (_, i) => `console.log(${i})`).join('\n')}\n}`)
14
+ assert(has(hugeFunction, 'max-lines-per-function'))
15
+ assert(has(await inspect(`export const work = () => {\n${'console.log(1)\n'.repeat(81)}}`), 'max-lines-per-function'))
16
+ const hugeFile = await inspect(Array.from({ length: 501 }, (_, i) => `export const value${i} = ${i}`).join('\n'))
17
+ assert(has(hugeFile, 'max-lines'))
18
+ assert(!has(await inspect(`// A comment\n${'\n// comment\n'.repeat(600)}export const x = 1`), 'max-lines'))
19
+ const complex = await inspect(`export function decide(value: number) { ${'if(value) console.log(value);'.repeat(21)} }`)
20
+ assert(has(complex, 'complexity'))
21
+ assert(has(await inspect('export function nested(x: boolean) { if(x){if(x){if(x){if(x){if(x){console.log(x)}}}}} }'), 'max-depth'))
22
+ assert(has(await inspect("import { value } from '../server/tool-handlers.js'"), 'import-boundary'))
23
+ assert(has(await inspect("export { value } from '../server/common.js'"), 'import-boundary'))
24
+ assert(has(await inspect("type Args = import('../server/common.js').ToolCallArgs"), 'import-boundary'))
25
+ assert(has(await inspect("const transport = require('../server/common.js')"), 'import-boundary'))
26
+ assert(has(await inspect('/* eslint complexity: \"off\" */\nexport const value = 1'), 'inline-suppression'))
27
+ assert(!has(await inspect("import type { Value } from '../contracts/value.js'"), 'import-boundary'))
28
+ assert(has(await inspect('// eslint-disable\nexport const x = 1'), 'inline-suppression'))
29
+ assert(has(await inspect('// eslint-disable-next-line complexity\nexport function f() {}'), 'inline-suppression'))
30
+ const any = await inspect('export function legacy(value: any) { return value }')
31
+ assert(has(any, 'explicit-any'))
32
+ assert.deepEqual(compare(any, baseline(any)), [])
33
+ assert(compare(any, []).some(e => e.includes('explicit-any')))
34
+ const grown = await inspect('export function legacy(value: any): any { return value }')
35
+ assert(compare(grown, baseline(any)).some(e => e.includes('explicit-any')))
36
+ const renamed = await inspect('export function replacement(value: any) { return value }')
37
+ assert(compare(renamed, baseline(any)).length >= 2, 'renames do not inherit debt')
38
+ const moved = any.map(f => ({ ...f, file: 'src/interact/moved.ts' }))
39
+ assert(compare(moved, baseline(any)).length >= 2, 'moves do not inherit debt')
40
+ assert(compare([], baseline(any)).some(e => e.includes('obsolete')))
41
+ assert(compare(any, baseline(grown)).some(e => e.includes('Lower baseline')))
42
+ assert(compare(any, [...baseline(any), ...baseline(any)]).some(e => e.includes('Duplicate')))
43
+ assert(compare(any, [{ ...baseline(any)[0], reason: '' }]).some(e => e.includes('Invalid')))
44
+ const collision = await inspect('function same(x: number) {return x} function same(x: number) {return x}')
45
+ assert(has(collision, 'duplicate-symbol'))
46
+ assert(compare(collision, baseline(collision)).some(e => e.includes('duplicate-symbol')), 'identity collisions cannot be baseline exceptions')
47
+ const suppressed = await inspect('// eslint-disable max-lines-per-function\n'+`function huge(){\n${'console.log(1)\n'.repeat(81)}}`)
48
+ assert(has(suppressed, 'max-lines-per-function'), 'inline directives cannot hide metrics')
49
+ const typeDebt = await inspect('type First = any; type Second = number')
50
+ const shiftedTypeDebt = await inspect('type First = number; type Second = any')
51
+ assert(compare(shiftedTypeDebt, baseline(typeDebt)).length >= 2, 'unrelated type declarations cannot offset each other')
52
+ const java = await measure('src/debugger/java/Fixture.java', 'int field;\n'.repeat(501))
53
+ assert(has(java, 'max-lines'))
54
+ assert.deepEqual(await measure('src/debugger/java/Fixture.java', '// comment\n'.repeat(501)), [])
55
+ assert(!has(await inspect('class Accessor { get value() { return 1 } set value(value: number) { console.log(value) } }'), 'duplicate-symbol'))
56
+ console.log('PASS structural metrics, debt ratchet, boundaries and suppression protection')
57
+ }
58
+ main().catch(error => { console.error(error); process.exitCode = 1 })
@@ -0,0 +1,24 @@
1
+ import assert from 'node:assert/strict'
2
+ import { readdir, readFile } from 'node:fs/promises'
3
+ import { measure } from '../../../scripts/code-quality.mjs'
4
+
5
+ // Refactored interaction services must meet defaults even if someone adds a legacy allowance.
6
+ const legacy = new Set(['android.ts', 'ios.ts', 'conditional-action.ts', 'classify.ts'])
7
+ async function sourceFiles(directory = ''): Promise<string[]> {
8
+ const entries = await readdir(`src/interact/${directory}`, { withFileTypes: true })
9
+ const paths: string[] = []
10
+ for (const entry of entries) {
11
+ const relative = directory ? `${directory}/${entry.name}` : entry.name
12
+ if (entry.isDirectory()) paths.push(...await sourceFiles(relative))
13
+ else if (relative.endsWith('.ts') && !legacy.has(relative)) paths.push(relative)
14
+ }
15
+ return paths
16
+ }
17
+ const files = await sourceFiles()
18
+ const baseline = JSON.parse(await readFile('scripts/code-quality-baseline.json', 'utf8')) as Array<{ file: string }>
19
+ for (const file of files) {
20
+ const path = `src/interact/${file}`
21
+ assert.deepEqual(await measure(path, await readFile(path, 'utf8')), [], `${path} must meet default structural and typing limits`)
22
+ assert(!baseline.some(entry => entry.file === path), `${path} cannot inherit legacy exceptions`)
23
+ }
24
+ console.log(`PASS default quality limits and no legacy exceptions across ${files.length} interaction modules`)
@@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'
4
4
  import path from 'node:path'
5
5
  import { handleToolCall, toolDefinitions } from '../../../src/server-core.js'
6
6
  import { ToolsObserve } from '../../../src/observe/index.js'
7
- import { ToolsNetwork } from '../../../src/network/index.js'
7
+ import { ToolsNetwork } from '../../../src/observe/network/index.js'
8
8
  import { deriveSnapshotMetadata, getSnapshotSessionId, resetSnapshotMetadataForTests } from '../../../src/observe/snapshot-metadata.js'
9
9
  import { _setDeviceListersForTests, _resetDeviceListersForTests } from '../../../src/utils/resolve-device.js'
10
10
  import { runWithBudget } from '../../../src/utils/operation-budget.js'
@@ -1,8 +1,8 @@
1
1
  import assert from 'node:assert/strict'
2
2
  import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv'
3
3
  import { toolDefinitions } from '../../../src/server/tool-definitions.js'
4
- import { conditionalTools, validateExpectedState } from '../../../src/server/condition-schema.js'
5
- import { journeySchema } from '../../../src/server/journey-schema.js'
4
+ import { conditionalTools, validateExpectedState } from '../../../src/server/schemas/condition-schema.js'
5
+ import { journeySchema } from '../../../src/server/schemas/journey-schema.js'
6
6
 
7
7
  const provider = new AjvJsonSchemaValidator()
8
8
  const expectedState = { snapshotSessionId: 'session-fixture', snapshotRevision: 1 }
@@ -0,0 +1,63 @@
1
+ import assert from 'node:assert/strict'
2
+ import { debuggerResponse } from '../../../src/server/handlers/debugger-response.js'
3
+ import { handleDebugger } from '../../../src/server/handlers/debugger-handlers.js'
4
+ import { debuggerContracts, debuggerTools } from '../../../src/server/schemas/debugger-schema.js'
5
+ import { debuggerSessions } from '../../../src/debugger/sessions.js'
6
+ import { DebuggerError } from '../../../src/debugger/worker.js'
7
+ import { diagnostic } from '../../../src/debugger/diagnostics.js'
8
+
9
+ const baseline = ['capabilities', 'attach', 'state']
10
+ const payload = {
11
+ success: false, debug_session_id: 'session', device: { id: 'device' }, target: { pid: 42 }, capabilities: { locals: true },
12
+ state: 'paused', captured_at_ms: 123, reason: null, stop: { stop_id: 'stop', thread_id: 'thread' },
13
+ limitations: ['SHALLOW_LOCALS'], cleanup: { status: 'failed', remaining_resources: ['forward:tcp:123'] },
14
+ error: { code: 'DEBUGGER_CLEANUP_FAILED', retryable: false, diagnostic: diagnostic('cleanup', 'operation_failed') },
15
+ execution_state: 'unknown', result: { frames: [{ frame_id: 'frame' }], truncated: true, availability: 'unknown' },
16
+ breakpoints: [], events: [], next_cursor: 4, overflow: true, timed_out: false, future_field: { preserve: true }
17
+ }
18
+ const expected = structuredClone(payload) as Record<string, unknown>
19
+ for (const key of ['device', 'target', 'capabilities']) delete expected[key]
20
+ const before = structuredClone(payload)
21
+ for (const tool of debuggerTools) {
22
+ const contract = debuggerContracts[tool.name]
23
+ assert.deepEqual(tool.inputSchema.properties.responseMode, { type: 'string', enum: ['compact', 'debug'], default: 'debug' })
24
+ const shape = contract.schema.shape
25
+ assert.equal(shape.responseMode.parse(undefined), 'debug')
26
+ for (const mode of ['compact', 'debug']) assert.equal(shape.responseMode.parse(mode), mode)
27
+ assert.equal(shape.responseMode.safeParse('full').success, false)
28
+ assert.deepEqual(debuggerResponse(contract.command, 'debug', payload), payload)
29
+ assert.deepEqual(debuggerResponse(contract.command, 'compact', payload), baseline.includes(contract.command) ? payload : expected)
30
+ const success = { ...payload, success: true, error: undefined, cleanup: { status: 'pending', remaining_resources: ['jdi_worker'] } }
31
+ assert.deepEqual(debuggerResponse(contract.command, 'compact', success), baseline.includes(contract.command) ? success : { ...expected, success: true, error: undefined, cleanup: success.cleanup })
32
+ const noSession = { ...payload, debug_session_id: null }
33
+ assert.deepEqual(debuggerResponse(contract.command, 'compact', noSession), noSession)
34
+ }
35
+ assert.deepEqual(payload, before, 'projection must not mutate nested or shared objects')
36
+
37
+ // This self-running test has its own process; restore the singleton seam in finally.
38
+ const original = debuggerSessions.command
39
+ try {
40
+ debuggerSessions.command = async (_id, _command, args) => {
41
+ assert.ok(!Object.hasOwn(args!, 'responseMode'), 'presentation input must not reach worker')
42
+ return { ...payload, success: true } as unknown as Awaited<ReturnType<typeof original>>
43
+ }
44
+ for (const mode of [undefined, 'debug', 'compact']) {
45
+ const response = await handleDebugger('get_debugger_locals', { debugSessionId: 'session', stopId: 'stop', frameId: 'frame', ...(mode ? { responseMode: mode } : {}) })
46
+ assert.equal(response.isError, undefined)
47
+ assert.deepEqual(JSON.parse(response.content[0].text), { ...(mode === 'compact' ? expected : payload), success: true })
48
+ }
49
+ for (const code of ['DEBUGGER_STALE_REFERENCE', 'DEBUGGER_NOT_FOUND', 'DEBUGGER_WORKER_FAILED', 'DEBUGGER_TIMEOUT', 'DEBUGGER_CANCELLED']) {
50
+ debuggerSessions.command = async () => { throw new DebuggerError(code, diagnostic('command', 'unknown')) }
51
+ for (const responseMode of ['compact', 'debug']) {
52
+ const response = await handleDebugger('resume_debugger', { debugSessionId: 'absent', stopId: 'old', responseMode })
53
+ const value = JSON.parse(response.content[0].text)
54
+ assert.equal(response.isError, true)
55
+ assert.equal(value.error.code, code)
56
+ assert.equal(Object.hasOwn(value, 'capabilities'), responseMode === 'debug')
57
+ assert.equal(value.debug_session_id, 'absent')
58
+ }
59
+ }
60
+ const invalid = await handleDebugger('detach_debugger', { debugSessionId: 'session', responseMode: 'full' })
61
+ assert.equal(JSON.parse(invalid.content[0].text).error.code, 'INVALID_ARGUMENT')
62
+ } finally { debuggerSessions.command = original }
63
+ console.log('PASS debugger response defaults, exact retention, failures, immutable projection and argument isolation')
@@ -0,0 +1,8 @@
1
+ import assert from 'node:assert/strict'
2
+ import { readFile } from 'node:fs/promises'
3
+ import { toolDefinitions } from '../../../src/server/tool-definitions.js'
4
+
5
+ // Captured before structural refactoring: changes require an intentional contract review.
6
+ const baseline = JSON.parse(await readFile(new URL('../../fixtures/contracts/tool-definitions.json', import.meta.url), 'utf8'))
7
+ assert.deepEqual(toolDefinitions, baseline, 'Public tool order, schemas, defaults and descriptions must remain compatible')
8
+ console.log('PASS public tool-definition compatibility baseline')