mobile-debug-mcp 0.33.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (448) hide show
  1. package/.agents/skills/mcp-builder/SKILL.md +44 -64
  2. package/.agents/skills/mcp-builder/references/build-flags.md +44 -43
  3. package/.agents/skills/mcp-builder/references/diagnostics-schema.md +4 -1
  4. package/.agents/skills/mcp-builder/references/toolchain-details.md +49 -62
  5. package/.agents/skills/spec-review/references/spec-review-checklist.md +7 -0
  6. package/.agents/skills/test-authoring/SKILL.md +7 -6
  7. package/.agents/skills/test-authoring/references/repo-test-layout.md +3 -1
  8. package/.agents/skills/test-authoring/references/test-authoring-workflow.md +16 -7
  9. package/.agents/skills/test-authoring/references/test-quality-checklist.md +2 -1
  10. package/.github/copilot-instructions.md +14 -36
  11. package/.github/workflows/ci.yml +23 -2
  12. package/AGENTS.md +12 -3
  13. package/CONTRIBUTING.md +79 -0
  14. package/README.md +158 -124
  15. package/agent-plugin/.claude-plugin/plugin.json +2 -2
  16. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  17. package/agent-plugin/.cursor-plugin/plugin.json +2 -2
  18. package/agent-plugin/.mcp.json +1 -1
  19. package/agent-plugin/README.md +20 -6
  20. package/agent-plugin/mcp.json +1 -1
  21. package/agent-plugin/plugin.json +1 -1
  22. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +113 -4
  23. package/dist/contracts/execution-controls.js +24 -0
  24. package/dist/contracts/selector.js +8 -0
  25. package/dist/contracts/target.js +15 -0
  26. package/dist/contracts/ui.js +1 -0
  27. package/dist/debugger/android.js +79 -0
  28. package/dist/debugger/diagnostics.js +54 -0
  29. package/dist/debugger/guard.js +30 -0
  30. package/dist/debugger/protocol.js +33 -0
  31. package/dist/debugger/sessions.js +304 -0
  32. package/dist/debugger/worker.js +176 -0
  33. package/dist/interact/adjustment/adjustment-context.js +41 -0
  34. package/dist/interact/adjustment/adjustment-gestures.js +55 -0
  35. package/dist/interact/adjustment/adjustment-plan.js +18 -0
  36. package/dist/interact/adjustment/adjustment-resolution.js +127 -0
  37. package/dist/interact/adjustment/adjustment-result.js +19 -0
  38. package/dist/interact/adjustment/adjustment-semantic.js +64 -0
  39. package/dist/interact/adjustment/adjustment-verification.js +35 -0
  40. package/dist/interact/adjustment/adjustment.js +52 -0
  41. package/dist/interact/adjustment/control-geometry.js +87 -0
  42. package/dist/interact/adjustment/control-values.js +136 -0
  43. package/dist/interact/adjustment/slider-labels.js +84 -0
  44. package/dist/interact/assertions/screen-assertion.js +37 -0
  45. package/dist/interact/assertions/state-assertion-context.js +35 -0
  46. package/dist/interact/assertions/state-assertion.js +76 -0
  47. package/dist/interact/assertions/state-comparison.js +141 -0
  48. package/dist/interact/assertions/visibility-assertion.js +34 -0
  49. package/dist/interact/conditional-action.js +12 -7
  50. package/dist/interact/index.js +65 -3074
  51. package/dist/interact/input-service.js +1 -0
  52. package/dist/interact/interaction-trace.js +21 -0
  53. package/dist/interact/model.js +1 -0
  54. package/dist/interact/observation.js +1 -0
  55. package/dist/interact/targeting/candidate-ancestor.js +58 -0
  56. package/dist/interact/targeting/candidate-result.js +23 -0
  57. package/dist/interact/targeting/candidate-scoring.js +50 -0
  58. package/dist/interact/targeting/element-helpers.js +216 -0
  59. package/dist/interact/targeting/find-element.js +65 -0
  60. package/dist/interact/targeting/nearby-control.js +56 -0
  61. package/dist/interact/targeting/resolved-elements.js +21 -0
  62. package/dist/interact/targeting/tap-target.js +17 -0
  63. package/dist/interact/targeting/target-scope.js +104 -0
  64. package/dist/interact/waits/legacy-log-wait.js +38 -0
  65. package/dist/interact/waits/legacy-wait-observations.js +103 -0
  66. package/dist/interact/waits/legacy-wait-state.js +34 -0
  67. package/dist/interact/waits/legacy-wait.js +48 -0
  68. package/dist/interact/waits/screen-wait.js +49 -0
  69. package/dist/interact/waits/ui-change-helpers.js +157 -0
  70. package/dist/interact/waits/ui-change-state.js +18 -0
  71. package/dist/interact/waits/ui-change-wait.js +77 -0
  72. package/dist/interact/waits/ui-wait-condition.js +56 -0
  73. package/dist/interact/waits/ui-wait-state.js +38 -0
  74. package/dist/interact/waits/ui-wait.js +106 -0
  75. package/dist/manage/android.js +55 -51
  76. package/dist/manage/index.js +38 -75
  77. package/dist/manage/ios/build-types.js +1 -0
  78. package/dist/manage/ios/build.js +210 -0
  79. package/dist/manage/ios/project-discovery.js +29 -0
  80. package/dist/manage/ios/scheme.js +18 -0
  81. package/dist/manage/ios.js +42 -256
  82. package/dist/observe/android/context.js +1 -0
  83. package/dist/observe/android/current-screen.js +55 -0
  84. package/dist/observe/android/fingerprint.js +13 -0
  85. package/dist/observe/android/log-stream.js +132 -0
  86. package/dist/observe/android/logs.js +107 -0
  87. package/dist/observe/android/screenshot.js +70 -0
  88. package/dist/observe/android/ui-tree.js +107 -0
  89. package/dist/observe/android.js +16 -520
  90. package/dist/observe/evidence.js +122 -76
  91. package/dist/observe/index.js +16 -263
  92. package/dist/observe/ios/context.js +16 -0
  93. package/dist/observe/ios/fingerprint.js +12 -0
  94. package/dist/observe/ios/log-stream.js +105 -0
  95. package/dist/observe/ios/logs.js +168 -0
  96. package/dist/observe/ios/screenshot.js +22 -0
  97. package/dist/observe/ios/ui-elements.js +241 -0
  98. package/dist/observe/ios/ui-tree.js +137 -0
  99. package/dist/observe/ios.js +17 -715
  100. package/dist/observe/logs/model.js +1 -0
  101. package/dist/{network → observe/network}/index.js +2 -2
  102. package/dist/observe/readiness-service.js +13 -2
  103. package/dist/observe/readiness.js +44 -18
  104. package/dist/observe/screenshot-encoding.js +41 -0
  105. package/dist/observe/snapshot-collection.js +79 -0
  106. package/dist/observe/snapshot-logs.js +30 -0
  107. package/dist/observe/snapshot-metadata.js +43 -31
  108. package/dist/observe/snapshot-semantic.js +115 -0
  109. package/dist/observe/system-trace-android.js +135 -0
  110. package/dist/observe/system-trace.js +294 -0
  111. package/dist/runtime/action-results.js +270 -0
  112. package/dist/runtime/clock.js +4 -0
  113. package/dist/server/common.js +1 -256
  114. package/dist/server/definitions/assert.js +162 -0
  115. package/dist/server/definitions/diagnostic.js +118 -0
  116. package/dist/server/definitions/input.js +274 -0
  117. package/dist/server/definitions/manage.js +194 -0
  118. package/dist/server/definitions/observe.js +149 -0
  119. package/dist/server/definitions/target.js +138 -0
  120. package/dist/server/definitions/wait.js +105 -0
  121. package/dist/server/handlers/action-execution.js +163 -0
  122. package/dist/server/handlers/correlation.js +23 -0
  123. package/dist/server/handlers/debugger-handlers.js +48 -0
  124. package/dist/server/handlers/debugger-response.js +9 -0
  125. package/dist/server/handlers/diagnostic.js +69 -0
  126. package/dist/server/handlers/handler-types.js +1 -0
  127. package/dist/server/handlers/input.js +154 -0
  128. package/dist/server/handlers/interact.js +113 -0
  129. package/dist/server/handlers/journey-assertions.js +35 -0
  130. package/dist/server/handlers/journey.js +147 -0
  131. package/dist/server/handlers/manage.js +119 -0
  132. package/dist/server/handlers/observe.js +95 -0
  133. package/dist/server/handlers/payload.js +4 -0
  134. package/dist/server/handlers/tool-handlers.js +160 -0
  135. package/dist/server/schemas/condition-schema.js +2 -0
  136. package/dist/server/schemas/debugger-schema.js +47 -0
  137. package/dist/server/{journey-schema.js → schemas/journey-schema.js} +8 -22
  138. package/dist/server/schemas/system-trace-schema.js +17 -0
  139. package/dist/server/tool-definitions.js +62 -1134
  140. package/dist/server-core.js +4 -4
  141. package/dist/server.js +34 -0
  142. package/dist/system/android-status.js +101 -0
  143. package/dist/system/android.js +37 -27
  144. package/dist/system/index.js +13 -438
  145. package/dist/system/ios-status.js +132 -0
  146. package/dist/system/ios.js +11 -8
  147. package/dist/system/model.js +1 -0
  148. package/dist/system/probes.js +105 -0
  149. package/dist/system/status-summary.js +43 -0
  150. package/dist/system/unsupported-status.js +52 -0
  151. package/dist/utils/android/commands.js +89 -0
  152. package/dist/utils/android/gradle.js +139 -0
  153. package/dist/utils/android/logs.js +103 -0
  154. package/dist/utils/android/ui-tree.js +233 -0
  155. package/dist/utils/android/utils.js +6 -564
  156. package/dist/utils/cli/idb/idb-helper.js +4 -6
  157. package/dist/utils/cli/idb/install-idb.js +13 -9
  158. package/dist/utils/cli/ios/preflight-ios.js +42 -38
  159. package/dist/utils/exec.js +2 -0
  160. package/dist/utils/ios/utils.js +45 -46
  161. package/dist/utils/java.js +75 -57
  162. package/dist/utils/operation-budget.js +2 -0
  163. package/dist/utils/resolve-device.js +6 -2
  164. package/dist/utils/ui/index.js +26 -26
  165. package/docs/CHANGELOG.md +48 -0
  166. package/docs/README.md +38 -0
  167. package/docs/benchmarks/README.md +18 -0
  168. package/docs/benchmarks/interaction-latency/README.md +14 -0
  169. package/docs/benchmarks/interaction-latency/final.md +4 -1
  170. package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
  171. package/docs/benchmarks/ui-tree-performance/README.md +24 -16
  172. package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
  173. package/docs/code-structure.md +118 -0
  174. package/docs/specs/001-state-verification/spec.md +16 -14
  175. package/docs/specs/002-richer-element-identity/spec.md +12 -10
  176. package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
  177. package/docs/specs/004-action-verification-routing/spec.md +3 -3
  178. package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
  179. package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
  180. package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
  181. package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
  182. package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
  183. package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
  184. package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
  185. package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
  186. package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
  187. package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
  188. package/docs/specs/014-actionability-resolution/spec.md +20 -18
  189. package/docs/specs/015-adjustable-control-support/spec.md +17 -15
  190. package/docs/specs/016-environment-tools/spec.md +20 -20
  191. package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
  192. package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
  193. package/docs/specs/020-conditional-actions/intent.md +5 -3
  194. package/docs/specs/020-conditional-actions/plan.md +8 -9
  195. package/docs/specs/020-conditional-actions/spec.md +4 -2
  196. package/docs/specs/021-app-readiness-signals/intent.md +4 -2
  197. package/docs/specs/021-app-readiness-signals/plan.md +7 -6
  198. package/docs/specs/021-app-readiness-signals/spec.md +3 -1
  199. package/docs/specs/022-correlated-evidence/intent.md +4 -4
  200. package/docs/specs/022-correlated-evidence/plan.md +10 -11
  201. package/docs/specs/022-correlated-evidence/spec.md +3 -1
  202. package/docs/specs/023-android-system-traces/device-evidence.json +38 -0
  203. package/docs/specs/023-android-system-traces/emulator-recheck.json +38 -0
  204. package/docs/specs/023-android-system-traces/intent.md +41 -0
  205. package/docs/specs/023-android-system-traces/plan.md +115 -0
  206. package/docs/specs/023-android-system-traces/spec.md +122 -0
  207. package/docs/specs/024-android-heap-dump-capture/intent.md +41 -0
  208. package/docs/specs/025-android-runtime-debugger/intent.md +41 -0
  209. package/docs/specs/025-android-runtime-debugger/plan.md +106 -0
  210. package/docs/specs/025-android-runtime-debugger/spec.md +183 -0
  211. package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
  212. package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
  213. package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
  214. package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
  215. package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
  216. package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
  217. package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
  218. package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
  219. package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
  220. package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
  221. package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
  222. package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
  223. package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
  224. package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
  225. package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
  226. package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
  227. package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
  228. package/docs/specs/030-android-coroutine-validation/report.md +240 -0
  229. package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
  230. package/docs/tools/TOOLS.md +31 -16
  231. package/docs/tools/{evidence.md → correlated-evidence.md} +14 -3
  232. package/docs/tools/debugger.md +180 -0
  233. package/docs/tools/interact.md +42 -20
  234. package/docs/tools/manage.md +25 -44
  235. package/docs/tools/observe.md +64 -25
  236. package/docs/tools/system-traces.md +134 -0
  237. package/docs/tools/system.md +12 -5
  238. package/package.json +11 -8
  239. package/scripts/architecture-graph.mjs +69 -0
  240. package/scripts/check-architecture.mjs +47 -0
  241. package/scripts/code-quality-baseline.json +1 -0
  242. package/scripts/code-quality.mjs +161 -0
  243. package/src/contracts/execution-controls.ts +26 -0
  244. package/src/contracts/selector.ts +17 -0
  245. package/src/contracts/target.ts +19 -0
  246. package/src/contracts/ui.ts +94 -0
  247. package/src/debugger/android.ts +66 -0
  248. package/src/debugger/diagnostics.ts +43 -0
  249. package/src/debugger/guard.ts +31 -0
  250. package/src/debugger/java/DebuggerWorker.java +166 -0
  251. package/src/debugger/protocol.ts +37 -0
  252. package/src/debugger/sessions.ts +327 -0
  253. package/src/debugger/worker.ts +135 -0
  254. package/src/interact/adjustment/adjustment-context.ts +83 -0
  255. package/src/interact/adjustment/adjustment-gestures.ts +48 -0
  256. package/src/interact/adjustment/adjustment-plan.ts +23 -0
  257. package/src/interact/adjustment/adjustment-resolution.ts +104 -0
  258. package/src/interact/adjustment/adjustment-result.ts +34 -0
  259. package/src/interact/adjustment/adjustment-semantic.ts +53 -0
  260. package/src/interact/adjustment/adjustment-verification.ts +60 -0
  261. package/src/interact/adjustment/adjustment.ts +49 -0
  262. package/src/interact/adjustment/control-geometry.ts +120 -0
  263. package/src/interact/adjustment/control-values.ts +141 -0
  264. package/src/interact/adjustment/slider-labels.ts +84 -0
  265. package/src/interact/assertions/screen-assertion.ts +39 -0
  266. package/src/interact/assertions/state-assertion-context.ts +48 -0
  267. package/src/interact/assertions/state-assertion.ts +64 -0
  268. package/src/interact/assertions/state-comparison.ts +168 -0
  269. package/src/interact/assertions/visibility-assertion.ts +41 -0
  270. package/src/interact/conditional-action.ts +18 -12
  271. package/src/interact/index.ts +76 -3491
  272. package/src/interact/input-service.ts +11 -0
  273. package/src/interact/interaction-trace.ts +36 -0
  274. package/src/interact/model.ts +109 -0
  275. package/src/interact/observation.ts +7 -0
  276. package/src/interact/targeting/candidate-ancestor.ts +49 -0
  277. package/src/interact/targeting/candidate-result.ts +24 -0
  278. package/src/interact/targeting/candidate-scoring.ts +42 -0
  279. package/src/interact/targeting/element-helpers.ts +243 -0
  280. package/src/interact/targeting/find-element.ts +47 -0
  281. package/src/interact/targeting/nearby-control.ts +46 -0
  282. package/src/interact/targeting/resolved-elements.ts +20 -0
  283. package/src/interact/targeting/tap-target.ts +22 -0
  284. package/src/interact/targeting/target-scope.ts +121 -0
  285. package/src/interact/waits/legacy-log-wait.ts +30 -0
  286. package/src/interact/waits/legacy-wait-observations.ts +55 -0
  287. package/src/interact/waits/legacy-wait-state.ts +48 -0
  288. package/src/interact/waits/legacy-wait.ts +34 -0
  289. package/src/interact/waits/screen-wait.ts +37 -0
  290. package/src/interact/waits/ui-change-helpers.ts +184 -0
  291. package/src/interact/waits/ui-change-state.ts +26 -0
  292. package/src/interact/waits/ui-change-wait.ts +59 -0
  293. package/src/interact/waits/ui-wait-condition.ts +52 -0
  294. package/src/interact/waits/ui-wait-state.ts +55 -0
  295. package/src/interact/waits/ui-wait.ts +84 -0
  296. package/src/manage/android.ts +56 -50
  297. package/src/manage/index.ts +63 -93
  298. package/src/manage/ios/build-types.ts +14 -0
  299. package/src/manage/ios/build.ts +230 -0
  300. package/src/manage/ios/project-discovery.ts +26 -0
  301. package/src/manage/ios/scheme.ts +16 -0
  302. package/src/manage/ios.ts +65 -298
  303. package/src/observe/android/context.ts +1 -0
  304. package/src/observe/android/current-screen.ts +63 -0
  305. package/src/observe/android/fingerprint.ts +13 -0
  306. package/src/observe/android/log-stream.ts +127 -0
  307. package/src/observe/android/logs.ts +110 -0
  308. package/src/observe/android/screenshot.ts +79 -0
  309. package/src/observe/android/ui-tree.ts +113 -0
  310. package/src/observe/android.ts +17 -531
  311. package/src/observe/evidence.ts +130 -71
  312. package/src/observe/index.ts +24 -286
  313. package/src/observe/ios/context.ts +22 -0
  314. package/src/observe/ios/fingerprint.ts +12 -0
  315. package/src/observe/ios/log-stream.ts +103 -0
  316. package/src/observe/ios/logs.ts +179 -0
  317. package/src/observe/ios/screenshot.ts +26 -0
  318. package/src/observe/ios/ui-elements.ts +266 -0
  319. package/src/observe/ios/ui-tree.ts +143 -0
  320. package/src/observe/ios.ts +18 -749
  321. package/src/observe/logs/model.ts +11 -0
  322. package/src/{network → observe/network}/index.ts +2 -2
  323. package/src/observe/readiness-service.ts +10 -3
  324. package/src/observe/readiness.ts +49 -22
  325. package/src/observe/screenshot-encoding.ts +32 -0
  326. package/src/observe/snapshot-collection.ts +69 -0
  327. package/src/observe/snapshot-logs.ts +26 -0
  328. package/src/observe/snapshot-metadata.ts +51 -31
  329. package/src/observe/snapshot-semantic.ts +153 -0
  330. package/src/observe/system-trace-android.ts +133 -0
  331. package/src/observe/system-trace.ts +227 -0
  332. package/src/runtime/action-results.ts +376 -0
  333. package/src/runtime/clock.ts +9 -0
  334. package/src/server/common.ts +9 -359
  335. package/src/server/definitions/assert.ts +164 -0
  336. package/src/server/definitions/diagnostic.ts +120 -0
  337. package/src/server/definitions/input.ts +276 -0
  338. package/src/server/definitions/manage.ts +196 -0
  339. package/src/server/definitions/observe.ts +151 -0
  340. package/src/server/definitions/target.ts +140 -0
  341. package/src/server/definitions/wait.ts +107 -0
  342. package/src/server/handlers/action-execution.ts +164 -0
  343. package/src/server/handlers/correlation.ts +20 -0
  344. package/src/server/handlers/debugger-handlers.ts +39 -0
  345. package/src/server/handlers/debugger-response.ts +7 -0
  346. package/src/server/handlers/diagnostic.ts +79 -0
  347. package/src/server/handlers/handler-types.ts +7 -0
  348. package/src/server/handlers/input.ts +173 -0
  349. package/src/server/handlers/interact.ts +135 -0
  350. package/src/server/handlers/journey-assertions.ts +39 -0
  351. package/src/server/handlers/journey.ts +132 -0
  352. package/src/server/handlers/manage.ts +141 -0
  353. package/src/server/handlers/observe.ts +116 -0
  354. package/src/server/handlers/payload.ts +36 -0
  355. package/src/server/handlers/tool-handlers.ts +159 -0
  356. package/src/server/schemas/condition-schema.ts +2 -0
  357. package/src/server/schemas/debugger-schema.ts +43 -0
  358. package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +8 -22
  359. package/src/server/schemas/system-trace-schema.ts +17 -0
  360. package/src/server/tool-definitions.ts +69 -1140
  361. package/src/server-core.ts +4 -4
  362. package/src/server.ts +25 -1
  363. package/src/system/android-status.ts +124 -0
  364. package/src/system/android.ts +33 -21
  365. package/src/system/index.ts +21 -523
  366. package/src/system/ios-status.ts +147 -0
  367. package/src/system/ios.ts +14 -10
  368. package/src/system/model.ts +41 -0
  369. package/src/system/probes.ts +114 -0
  370. package/src/system/status-summary.ts +47 -0
  371. package/src/system/unsupported-status.ts +55 -0
  372. package/src/types.ts +16 -106
  373. package/src/utils/android/commands.ts +81 -0
  374. package/src/utils/android/gradle.ts +132 -0
  375. package/src/utils/android/logs.ts +103 -0
  376. package/src/utils/android/ui-tree.ts +239 -0
  377. package/src/utils/android/utils.ts +16 -556
  378. package/src/utils/cli/idb/check-idb.ts +4 -4
  379. package/src/utils/cli/idb/idb-helper.ts +6 -8
  380. package/src/utils/cli/idb/install-idb.ts +15 -9
  381. package/src/utils/cli/ios/preflight-ios.ts +39 -31
  382. package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
  383. package/src/utils/exec.ts +2 -0
  384. package/src/utils/ios/utils.ts +57 -61
  385. package/src/utils/java.ts +71 -51
  386. package/src/utils/operation-budget.ts +2 -0
  387. package/src/utils/resolve-device.ts +11 -6
  388. package/src/utils/ui/index.ts +28 -28
  389. package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
  390. package/test/device/automated/debugger/coroutine-support.ts +140 -0
  391. package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
  392. package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
  393. package/test/device/automated/debugger/runtime.android.test.ts +219 -0
  394. package/test/device/automated/interact/interaction.android.test.ts +56 -0
  395. package/test/device/automated/observe/system-trace.smoke.ts +104 -0
  396. package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
  397. package/test/fixtures/contracts/tool-definitions.json +3536 -0
  398. package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
  399. package/test/fixtures/coroutine-app/README.md +48 -0
  400. package/test/fixtures/coroutine-app/build.ts +35 -0
  401. package/test/fixtures/debugger-app/MainActivity.java +67 -0
  402. package/test/fixtures/debugger-app/MainActivity.kt +32 -0
  403. package/test/fixtures/debugger-app/NoLocals.java +7 -0
  404. package/test/fixtures/debugger-app/README.md +65 -0
  405. package/test/fixtures/debugger-app/build.ts +39 -0
  406. package/test/fixtures/latency-journey-app/README.md +2 -0
  407. package/test/fixtures/readiness-app/README.md +2 -0
  408. package/test/helpers/perfetto.ts +59 -0
  409. package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
  410. package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
  411. package/test/unit/debugger/diagnostics.test.ts +80 -0
  412. package/test/unit/debugger/guards.test.ts +50 -0
  413. package/test/unit/debugger/native.test.ts +33 -0
  414. package/test/unit/debugger/preparation.test.ts +32 -0
  415. package/test/unit/debugger/protocol.test.ts +15 -0
  416. package/test/unit/debugger/sessions.test.ts +83 -0
  417. package/test/unit/interact/adjustment_service.test.ts +42 -0
  418. package/test/unit/interact/adjustment_verification.test.ts +35 -0
  419. package/test/unit/interact/clock_dependencies.test.ts +63 -0
  420. package/test/unit/interact/element_helpers.test.ts +19 -0
  421. package/test/unit/interact/legacy_wait_service.test.ts +17 -0
  422. package/test/unit/interact/state_comparison.test.ts +20 -0
  423. package/test/unit/interact/target_scope.test.ts +35 -0
  424. package/test/unit/network/get_network_activity.test.ts +1 -1
  425. package/test/unit/observe/readiness.test.ts +3 -1
  426. package/test/unit/observe/screenshot-encoding.test.ts +30 -0
  427. package/test/unit/observe/snapshot-logs.test.ts +14 -0
  428. package/test/unit/observe/system-trace-android.test.ts +52 -0
  429. package/test/unit/observe/system-trace.test.ts +166 -0
  430. package/test/unit/quality/architecture.test.ts +60 -0
  431. package/test/unit/quality/code-quality.test.ts +58 -0
  432. package/test/unit/quality/interaction_boundaries.test.ts +24 -0
  433. package/test/unit/server/conditional_actions.test.ts +1 -1
  434. package/test/unit/server/conditional_schema.test.ts +2 -2
  435. package/test/unit/server/debugger-response.test.ts +63 -0
  436. package/test/unit/server/system-trace.test.ts +64 -0
  437. package/test/unit/server/tool_definitions_parity.test.ts +8 -0
  438. package/dist/server/tool-handlers.js +0 -893
  439. package/docs/ROADMAP.md +0 -646
  440. package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
  441. package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
  442. package/docs/specs/baseline-spec-v0.md +0 -312
  443. package/docs/specs/mcp-tooling-spec-v1.md +0 -446
  444. package/src/server/tool-handlers.ts +0 -923
  445. /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
  446. /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
  447. /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
  448. /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
@@ -0,0 +1,80 @@
1
+ import assert from 'node:assert/strict'
2
+ import { mkdtemp, writeFile, rm } from 'node:fs/promises'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { diagnostic, stages, reasons, classify, parseDiagnostic } from '../../../src/debugger/diagnostics.js'
6
+ import { DebuggerError, JdiWorker, withDebuggerBudget, type WorkerEvent } from '../../../src/debugger/worker.js'
7
+ import { parseWorkerMessage } from '../../../src/debugger/protocol.js'
8
+ import { handleDebugger } from '../../../src/server/handlers/debugger-handlers.js'
9
+
10
+ const secret = 'PRIVATE_LOCALS_TOKEN_/private/path'
11
+ for (const stage of stages) for (const reason of reasons) {
12
+ const value = diagnostic(stage, reason)
13
+ assert.ok(Buffer.byteLength(JSON.stringify(value)) <= 512)
14
+ assert.ok(value.recovery.length <= 240 && /^[\x00-\x7f]*$/.test(value.recovery))
15
+ assert.deepEqual(Object.keys(value), ['stage', 'reason', 'recovery'])
16
+ }
17
+ assert.deepEqual(parseDiagnostic({ stage: 'attach', reason: 'connection_refused', recovery: secret, raw: secret }), diagnostic('attach', 'connection_refused'))
18
+ assert.equal(classify(new Error(secret), 'attach', 'DEBUGGER_ATTACH_FAILED').diagnostic?.reason, 'unknown')
19
+ assert.equal(classify(new DebuggerError('DEBUGGER_STALE_REFERENCE'), 'command', 'DEBUGGER_WORKER_FAILED').diagnostic, undefined)
20
+ for (const hostile of [secret, { stage: secret, reason: 'unknown' }, { stage: 'attach', reason: secret }]) {
21
+ assert.throws(() => parseWorkerMessage({ id: 1, error: { code: 'DEBUGGER_WORKER_FAILED', diagnostic: hostile } }), /INVALID_WORKER_DIAGNOSTIC/)
22
+ }
23
+ assert.throws(() => parseWorkerMessage({ id: 1, error: { code: secret } }), /INVALID_WORKER_ERROR/)
24
+ const cancelled = new AbortController(); cancelled.abort()
25
+ const response = await handleDebugger('get_debugger_state', { debugSessionId: 'absent' }, cancelled.signal)
26
+ assert.equal(response.isError, true)
27
+ const payload = JSON.parse(response.content[0].text)
28
+ assert.equal(payload.error.code, 'DEBUGGER_CANCELLED')
29
+ assert.deepEqual(payload.error.diagnostic, diagnostic('unknown', 'cancelled'))
30
+ assert.equal((await handleDebugger('get_debugger_state', {})).isError, true)
31
+
32
+ const directory = await mkdtemp(path.join(os.tmpdir(), 'debugger-diagnostics-test-'))
33
+ let index = 0
34
+ async function worker(body: string, onEvent: (event: WorkerEvent) => void = () => {}) {
35
+ const file = path.join(directory, `worker-${index++}`)
36
+ await writeFile(file, `#!${process.execPath}\n${body}\n`, { mode: 0o700 })
37
+ return new JdiWorker(file, directory, onEvent)
38
+ }
39
+ async function fails(body: string, stage: string, reason: string) {
40
+ const w = await worker(body)
41
+ try {
42
+ await assert.rejects(w.call('attach', {}, 3000), (error: unknown) => {
43
+ assert.ok(error instanceof DebuggerError)
44
+ assert.equal(error.code, 'DEBUGGER_WORKER_FAILED')
45
+ assert.deepEqual(error.diagnostic, diagnostic(stage as typeof stages[number], reason as typeof reasons[number]))
46
+ assert.ok(!JSON.stringify(error).includes(secret))
47
+ return true
48
+ })
49
+ } finally { await w.close() }
50
+ }
51
+ try {
52
+ const missing = new JdiWorker(path.join(directory, 'missing'), directory, () => {})
53
+ try { await assert.rejects(missing.call('attach'), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.reason === 'spawn_failed') } finally { await missing.close() }
54
+ await fails(`process.stderr.write(${JSON.stringify(secret)}); process.exit(7)`, 'worker_start', 'process_exited')
55
+ await fails(`process.stdout.write(${JSON.stringify(secret + '\n')}); setInterval(()=>{},1000)`, 'ipc', 'invalid_message')
56
+ await fails(`process.stdout.write('x'.repeat(1048577)+'\\n'); setInterval(()=>{},1000)`, 'ipc', 'message_limit')
57
+ const attach = await worker(`process.stdin.once('data', data => { const {id}=JSON.parse(data); console.log(JSON.stringify({id,error:{code:'DEBUGGER_WORKER_FAILED',diagnostic:{stage:'attach',reason:'connection_refused',raw:${JSON.stringify(secret)}}}})) })`)
58
+ try { await assert.rejects(attach.call('attach'), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.reason === 'connection_refused') } finally { await attach.close() }
59
+ let ready = () => {}
60
+ const readyPromise = new Promise<void>(resolve => { ready = resolve })
61
+ const brokenPipe = await worker(`require('node:fs').closeSync(0); console.log(JSON.stringify({event:'ready'})); setInterval(()=>{},1000)`, () => ready())
62
+ try {
63
+ await readyPromise
64
+ await assert.rejects(brokenPipe.call('state'), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.reason === 'write_failed')
65
+ } finally { await brokenPipe.close() }
66
+ const afterReply = await worker(`process.stdin.once('data',data=>{const {id}=JSON.parse(data); console.log(JSON.stringify({id,result:{}})); setTimeout(()=>process.exit(7),100)})`)
67
+ try {
68
+ await afterReply.call('state')
69
+ await assert.rejects(afterReply.call('state'), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.stage === 'ipc' && e.diagnostic.reason === 'process_exited')
70
+ } finally { await afterReply.close() }
71
+ const idle = await worker('process.stdin.resume()')
72
+ try {
73
+ await assert.rejects(idle.call('stack', {}, 50), (e: unknown) => e instanceof DebuggerError && e.diagnostic?.stage === 'command' && e.diagnostic.reason === 'timeout')
74
+ const controller = new AbortController()
75
+ const call = withDebuggerBudget(1000, controller.signal, () => idle.call('attach'))
76
+ controller.abort()
77
+ await assert.rejects(call, (e: unknown) => e instanceof DebuggerError && e.diagnostic?.stage === 'attach' && e.diagnostic.reason === 'cancelled')
78
+ } finally { await idle.close() }
79
+ } finally { await rm(directory, { recursive: true, force: true }) }
80
+ console.log('PASS diagnostic bounds/privacy, strict IPC, real worker spawn/exit/malformed/oversize, attach failure, timeout/cancel and MCP wrapping')
@@ -0,0 +1,50 @@
1
+ import assert from 'node:assert/strict'
2
+ import { debuggerSessions } from '../../../src/debugger/sessions.js'
3
+ import { withDebuggerGuard, checkDebuggerGuard } from '../../../src/debugger/guard.js'
4
+ import { handleToolCall } from '../../../src/server-core.js'
5
+ import { _setDeviceListersForTests, _resetDeviceListersForTests } from '../../../src/utils/resolve-device.js'
6
+ import { ToolsObserve } from '../../../src/observe/index.js'
7
+ import { ToolsInteract } from '../../../src/interact/index.js'
8
+ import { getAppReadiness } from '../../../src/observe/readiness-service.js'
9
+ const originalPause = debuggerSessions.paused
10
+ const originalActive = debuggerSessions.hasActiveSessions
11
+ const originalTree = ToolsObserve.getUITreeHandler
12
+ const originalWait = ToolsInteract.waitForUIHandler
13
+ let paused = true
14
+ const pause: any = { debug_session_id: 'session', target: { app_id: 'test.app', pid: 10 }, state: 'paused', captured_at_ms: 1000 }
15
+ debuggerSessions.paused = device => paused && device === 'fixture' ? pause : null
16
+ debuggerSessions.hasActiveSessions = () => true
17
+ _setDeviceListersForTests({ listAndroidDevices: async () => [{ id: 'fixture', platform: 'android' } as any] })
18
+ const payload = (response: any) => JSON.parse(response.content.find((c: any) => c.type === 'text').text)
19
+ try {
20
+ ToolsObserve.getUITreeHandler = async () => { throw new Error('Paused readiness must not acquire a UI tree') }
21
+ const readiness = await getAppReadiness({ platform: 'android', deviceId: 'fixture' })
22
+ assert.equal(readiness.readiness.state, 'debugger_paused')
23
+ const wait = await getAppReadiness({ platform: 'android', deviceId: 'fixture' }, true)
24
+ assert.equal(wait.failure_code, 'DEBUGGER_PAUSED')
25
+ const blocked = payload(await handleToolCall('tap', { platform: 'android', deviceId: 'fixture', x: 1, y: 1 }))
26
+ assert.equal(blocked.delivery_status, 'not_delivered')
27
+ await withDebuggerGuard('android', 'other', async () => checkDebuggerGuard(true))
28
+ await assert.rejects(withDebuggerGuard('android', 'fixture', async () => checkDebuggerGuard()), /DEBUGGER_PAUSED/)
29
+ paused = false
30
+ let calls = 0
31
+ ToolsInteract.waitForUIHandler = async () => { calls++; if (calls === 2) paused = true; return { status: 'success' } as any }
32
+ const journey = payload(await handleToolCall('run_journey', { platform: 'android', deviceId: 'fixture', responseMode: 'compact', steps: [
33
+ { id: 'first', type: 'wait', selector: { text: 'First' } },
34
+ { id: 'second', type: 'wait', selector: { text: 'Second' } },
35
+ { id: 'third', type: 'wait', selector: { text: 'Third' } }
36
+ ] }))
37
+ assert.equal(journey.steps[0].status, 'passed', 'completed steps survive a later pause')
38
+ assert.equal(journey.steps[1].success, false)
39
+ assert.equal(journey.steps[1].result.failure_code, 'DEBUGGER_PAUSED')
40
+ assert.equal(calls, 2, 'no action queued or retried after pause')
41
+ _setDeviceListersForTests({ listAndroidDevices: async () => { throw new Error('resolution unavailable') } })
42
+ const failedResolution = payload(await handleToolCall('tap', { platform: 'android', deviceId: 'fixture', x: 1, y: 1 }))
43
+ assert.equal(failedResolution.error.tool, 'tap')
44
+ assert.match(failedResolution.error.message, /resolution unavailable/, 'resolution errors retain the MCP error envelope')
45
+ console.log('PASS debugger readiness without UI acquisition, device scoping, dispatch guard, mid-journey pause preservation')
46
+ } finally {
47
+ debuggerSessions.paused = originalPause; debuggerSessions.hasActiveSessions = originalActive
48
+ ToolsObserve.getUITreeHandler = originalTree; ToolsInteract.waitForUIHandler = originalWait
49
+ _resetDeviceListersForTests()
50
+ }
@@ -0,0 +1,33 @@
1
+ import assert from 'node:assert/strict'
2
+ import { AndroidDebugNative } from '../../../src/debugger/android.js'
3
+ import { DebuggerError, debuggerRemaining, withDebuggerBudget } from '../../../src/debugger/worker.js'
4
+ class Native extends AndroidDebugNative {
5
+ calls: string[][] = []
6
+ forwards = 'device tcp:1234 jdwp:10\nother tcp:3456 jdwp:20'
7
+ async adb(_device: string, args: string[]) {
8
+ this.calls.push(args)
9
+ if (args[0] === 'forward' && args[1] === '--list') return this.forwards
10
+ if (args.at(-1)?.endsWith('/stat')) return '10 (complex process name) S ' + Array(18).fill('0').join(' ') + ' 999 0'
11
+ if (args.at(-1)?.endsWith('/status')) return 'Name:\ttest\nUid:\t42\t42\t42\t42'
12
+ if (args.at(-1)?.endsWith('/cmdline')) return 'test.app\0'
13
+ return ''
14
+ }
15
+ }
16
+ const native = new Native()
17
+ await native.removeForward('device', 1234, 10)
18
+ assert.deepEqual(native.calls.at(-1), ['forward', '--remove', 'tcp:1234'])
19
+ await assert.rejects(native.removeForward('device', 3456, 10), /DEBUGGER_CLEANUP_FAILED/)
20
+ const before = native.calls.length
21
+ await native.removeForward('device', 9999, 10)
22
+ assert.equal(native.calls.length, before + 1, 'absent owned forward requires no removal')
23
+ const target = { app_id: 'test.app', pid: 10, process_name: 'test.app', start_identity: '', uid: 42 }
24
+ assert.equal((await native.identity('device', target)).start_identity, '999')
25
+ await assert.rejects(native.identity('device', { ...target, start_identity: 'old' }), /PROCESS_CHANGED/)
26
+ await assert.rejects(native.identity('device', { ...target, uid: 44 }), /PROCESS_CHANGED/)
27
+ await assert.rejects(native.candidates('device', 'test.app;evil'), /INVALID_ARGUMENT/)
28
+ const aborted = new AbortController(); aborted.abort()
29
+ await assert.rejects(withDebuggerBudget(100, aborted.signal, async () => debuggerRemaining(10)), /DEBUGGER_CANCELLED/)
30
+ await assert.rejects(withDebuggerBudget(-1, undefined, async () => debuggerRemaining(10)), /DEBUGGER_TIMEOUT/)
31
+ assert.equal(await withDebuggerBudget(100, undefined, async () => debuggerRemaining(10)), 10)
32
+ assert.equal(new DebuggerError('DEBUGGER_TIMEOUT').code, 'DEBUGGER_TIMEOUT')
33
+ console.log('PASS debugger native identity, PID reuse, forward ownership, deadlines and cancellation')
@@ -0,0 +1,32 @@
1
+ import assert from 'node:assert/strict'
2
+ import { execFile } from 'node:child_process'
3
+ import { promisify } from 'node:util'
4
+ import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
5
+ import os from 'node:os'
6
+ import path from 'node:path'
7
+ const exec = promisify(execFile)
8
+ const directory = await mkdtemp(path.join(os.tmpdir(), 'debugger-fake-jdk-'))
9
+ const secret = 'SECRET_COMPILER_OUTPUT'
10
+ const run = async () => {
11
+ const code = `import {prepareWorker} from './src/debugger/worker.ts'; try {await prepareWorker(); process.exitCode=1} catch(e) {console.log(JSON.stringify({code:e.code,diagnostic:e.diagnostic}))}`
12
+ const { stdout, stderr } = await exec(process.execPath, ['--import', 'tsx', '--input-type=module', '-e', code], {
13
+ env: { ...process.env, ANDROID_STUDIO_JDK: directory }, timeout: 10000
14
+ })
15
+ assert.ok(!stdout.includes(secret)); assert.ok(!stderr.includes(secret))
16
+ return JSON.parse(stdout)
17
+ }
18
+ try {
19
+ await mkdir(path.join(directory, 'bin'))
20
+ const java = path.join(directory, 'bin', 'java')
21
+ await writeFile(java, `#!${process.execPath}\nconsole.error('openjdk version "17.0.1"');`, { mode: 0o700 })
22
+ await writeFile(path.join(directory, 'bin', 'javac'), `#!${process.execPath}\nconsole.error('${secret}');process.exit(1)`, { mode: 0o700 })
23
+ const compile = await run()
24
+ assert.equal(compile.code, 'DEBUGGER_UNSUPPORTED')
25
+ assert.equal(compile.diagnostic.stage, 'compilation'); assert.equal(compile.diagnostic.reason, 'operation_failed')
26
+ // Discovery succeeds; the actual bounded version subprocess then fails.
27
+ await writeFile(java, `#!${process.execPath}\nconst fs=require('node:fs');const marker=${JSON.stringify(path.join(directory, 'seen'))};if(fs.existsSync(marker)){console.error('${secret}');process.exit(1)}fs.writeFileSync(marker,'');console.error('openjdk version "17.0.1"');`, { mode: 0o700 })
28
+ const prerequisite = await run()
29
+ assert.equal(prerequisite.code, 'DEBUGGER_ATTACH_FAILED')
30
+ assert.equal(prerequisite.diagnostic.stage, 'prerequisite'); assert.equal(prerequisite.diagnostic.reason, 'unavailable')
31
+ } finally { await rm(directory, { recursive: true, force: true }) }
32
+ console.log('PASS isolated fake-JDK compilation/prerequisite classification without compiler output leakage')
@@ -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')
@@ -0,0 +1,83 @@
1
+ import assert from 'node:assert/strict'
2
+ import { DebuggerSessions, debuggerExpiry } from '../../../src/debugger/sessions.js'
3
+ import { DebuggerError, type WorkerEvent } from '../../../src/debugger/worker.js'
4
+ import type { DebugNative, DebugTarget } from '../../../src/debugger/android.js'
5
+ import { debuggerContracts, debuggerTools } from '../../../src/server/schemas/debugger-schema.js'
6
+ assert.equal(debuggerExpiry(0, 0, 0, 59999), null)
7
+ assert.equal(debuggerExpiry(0, 60000, 0, 60000), 'pause_limit', 'polling cannot extend pause expiry')
8
+ assert.equal(debuggerExpiry(0, 0, undefined, 300000), 'idle_limit')
9
+ assert.equal(debuggerExpiry(0, 1800000, undefined, 1800000), 'session_limit', 'activity cannot extend lifetime')
10
+ const target: DebugTarget = { app_id: 'test.app', pid: 123, uid: 456, process_name: 'test.app', start_identity: '100' }
11
+ let candidates = [target]
12
+ let removed = 0
13
+ let emit: (event: WorkerEvent) => void = () => {}
14
+ let close = 0
15
+ let failCleanup = false
16
+ let failCommand = ''
17
+ const native: DebugNative = {
18
+ candidates: async () => candidates, identity: async () => target, forward: async () => 12345,
19
+ removeForward: async () => { if (failCleanup) throw new Error('offline'); removed++ }
20
+ }
21
+ const sessions = new DebuggerSessions(native, async event => {
22
+ emit = event
23
+ return { call: async (command: string) => { if (command === failCommand) throw new DebuggerError('DEBUGGER_TIMEOUT'); return { state: 'running', breakpoints: [] } }, close: async () => { close++ } }
24
+ }, async () => ({ java: 'java', directory: '/unused' }))
25
+ try {
26
+ candidates = [target, { ...target, pid: 124 }]
27
+ const ambiguous = await sessions.attach('emulator', 'test.app')
28
+ assert.equal(ambiguous.error.code, 'PROCESS_AMBIGUOUS')
29
+ assert.equal(removed, 0)
30
+ candidates = [target]
31
+ const first = await sessions.attach('emulator', 'test.app')
32
+ const id = first.debug_session_id
33
+ assert.equal(first.state, 'running')
34
+ await assert.rejects(sessions.attach('emulator', 'test.app'), /DEBUGGER_BUSY/)
35
+ emit({ event: 'stopped', stop_id: 'stop-1', thread_id: 't1', stops: [] })
36
+ assert.equal(sessions.paused('emulator')?.stop.stop_id, 'stop-1')
37
+ assert.equal(sessions.paused('other'), null)
38
+ assert.throws(() => sessions.assertRunning('emulator'), /DEBUGGER_PAUSED/)
39
+ const pendingWait = sessions.command(id, 'events', { afterSequence: 1 }, 500)
40
+ await sessions.command(id, 'resume', { stopId: 'stop-1' })
41
+ assert.equal(sessions.paused('emulator'), null)
42
+ await assert.rejects(sessions.command(id, 'locals', { stopId: 'stop-1', frameId: 'old' }), /DEBUGGER_STALE_REFERENCE/)
43
+ emit({ event: 'stopped', stop_id: 'stop-2', thread_id: 't2' })
44
+ assert.equal((await pendingWait).events[0].stop_id, 'stop-2', 'event wait cannot block resume')
45
+ for (let i = 0; i < 260; i++) emit({ event: 'breakpoint', i })
46
+ const events = await sessions.command(id, 'events', { afterSequence: 0 }, 100)
47
+ assert.equal(events.overflow, true); assert.equal(events.events.length, 100)
48
+ await assert.rejects(sessions.command(id, 'events', { afterSequence: 9999 }, 100), /INVALID_ARGUMENT/)
49
+ const controller = new AbortController(); controller.abort()
50
+ await assert.rejects(sessions.command(id, 'events', { afterSequence: 262, signal: controller.signal }, 100), /DEBUGGER_CANCELLED/)
51
+ failCommand = 'locals'
52
+ await assert.rejects(sessions.command(id, 'locals', { stopId: 'stop-2' }, 100), /DEBUGGER_TIMEOUT/)
53
+ assert.equal(sessions.paused('emulator')?.state, 'paused', 'inspection timeout preserves known suspension')
54
+ failCommand = 'step'
55
+ await assert.rejects(sessions.command(id, 'step', { stopId: 'stop-2', threadId: 't2' }, 100), /DEBUGGER_TIMEOUT/)
56
+ assert.equal((await sessions.command(id, 'state')).cleanup.status, 'complete')
57
+ assert.equal(close, 1); assert.equal(removed, 1)
58
+ failCommand = ''
59
+ const second = await sessions.attach('emulator', 'test.app')
60
+ failCleanup = true
61
+ const partial = await sessions.command(second.debug_session_id, 'detach')
62
+ assert.equal(partial.success, false); assert.equal(partial.cleanup.status, 'failed')
63
+ await assert.rejects(sessions.attach('emulator', 'test.app'), /DEBUGGER_BUSY/)
64
+ failCleanup = false
65
+ const retry = await sessions.command(second.debug_session_id, 'detach')
66
+ assert.equal(retry.cleanup.status, 'complete'); assert.equal(close, 2)
67
+ assert.equal((await sessions.command(second.debug_session_id, 'detach')).cleanup.status, 'complete')
68
+ const third = await sessions.attach('emulator', 'test.app')
69
+ emit({ event: 'worker_failed' })
70
+ const crashed = await sessions.command(third.debug_session_id, 'state')
71
+ assert.equal(crashed.state, 'failed')
72
+ assert.equal(crashed.cleanup.status, 'complete')
73
+ assert.equal(crashed.error.code, 'DEBUGGER_WORKER_FAILED')
74
+ assert.equal(sessions.paused('emulator'), null)
75
+ for (const tool of debuggerTools) {
76
+ const contract = debuggerContracts[tool.name]
77
+ assert.equal(tool.inputSchema.additionalProperties, false)
78
+ assert.equal(contract.schema.safeParse({ ...Object.fromEntries(tool.inputSchema.required.map(k => [k, 'x'])), unexpected: true }).success, false)
79
+ }
80
+ assert.equal(debuggerContracts.attach_debugger.schema.safeParse({ platform: 'android', appId: 'test.app' }).success, false)
81
+ assert.equal(debuggerContracts.set_debugger_breakpoint.schema.safeParse({ debugSessionId: 's', className: '$(bad)', line: 2 }).success, false)
82
+ console.log('PASS debugger session ownership, stop epochs, event concurrency/overflow, timeout recovery, cleanup retries, strict contracts')
83
+ } finally { await sessions.shutdown() }
@@ -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
 
@@ -32,8 +32,10 @@ for (const extra of [{ error: 'offline' }, { elements: [] }, { captured_at_ms: 9
32
32
  for (let index = 0; index < 20; index++) tracker.observe(sample(), 10000, 8000 + index)
33
33
  assert.equal(tracker.summary.evidence.length, 8)
34
34
  assert.equal(tracker.summary.capabilities.unresponsive, false)
35
- assert.equal(tracker.summary.capabilities.debugger_paused, false)
35
+ assert.equal(tracker.summary.capabilities.debugger_paused, true)
36
36
  assert.ok(tracker.deadline().limitations.includes('DEADLINE_EXCEEDED'))
37
37
  assert.equal(new ReadinessTracker('android', 'fixture').observe(sample(), 10000, 9000).state, 'unknown', 'no cross-request history')
38
38
  assert.ok(!JSON.stringify(tracker.summary).includes('Ready'), 'evidence contains codes rather than private labels')
39
+ assert.equal(new ReadinessTracker('ios', 'fixture').summary.capabilities.debugger_paused, false)
40
+ assert.ok(tracker.summary.limitations.includes('MANAGED_DEBUGGER_ONLY'))
39
41
  console.log('Readiness classifier: PASS')
@@ -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')