mobile-debug-mcp 0.34.0 → 0.36.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 (500) 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/.cursor-plugin/marketplace.json +1 -1
  11. package/.github/copilot-instructions.md +14 -36
  12. package/.github/workflows/ci.yml +4 -4
  13. package/AGENTS.md +12 -3
  14. package/CONTRIBUTING.md +79 -0
  15. package/README.md +218 -129
  16. package/agent-plugin/.antigravity-plugin/README.md +48 -0
  17. package/agent-plugin/.antigravity-plugin/plugin.json +18 -0
  18. package/agent-plugin/.claude-plugin/plugin.json +2 -2
  19. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  20. package/agent-plugin/.cursor-plugin/plugin.json +2 -2
  21. package/agent-plugin/.mcp.json +1 -1
  22. package/agent-plugin/README.md +50 -11
  23. package/agent-plugin/cursor/.cursor-plugin/plugin.json +23 -0
  24. package/agent-plugin/cursor/LICENSE +201 -0
  25. package/agent-plugin/cursor/README.md +9 -0
  26. package/agent-plugin/cursor/skills/mobile-debug-mcp/SKILL.md +253 -0
  27. package/agent-plugin/mcp.json +1 -1
  28. package/agent-plugin/mcp_config.json +13 -0
  29. package/agent-plugin/plugin.json +1 -1
  30. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +62 -17
  31. package/dist/contracts/execution-controls.js +24 -0
  32. package/dist/contracts/selector.js +8 -0
  33. package/dist/contracts/target.js +15 -0
  34. package/dist/contracts/ui.js +1 -0
  35. package/dist/debugger/android.js +2 -1
  36. package/dist/debugger/diagnostics.js +54 -0
  37. package/dist/debugger/guard.js +4 -4
  38. package/dist/debugger/ios-worker.js +62 -0
  39. package/dist/debugger/ios.js +57 -0
  40. package/dist/debugger/protocol.js +33 -0
  41. package/dist/debugger/sessions.js +133 -74
  42. package/dist/debugger/worker.js +59 -46
  43. package/dist/interact/adjustment/adjustment-context.js +41 -0
  44. package/dist/interact/adjustment/adjustment-gestures.js +55 -0
  45. package/dist/interact/adjustment/adjustment-plan.js +18 -0
  46. package/dist/interact/adjustment/adjustment-resolution.js +127 -0
  47. package/dist/interact/adjustment/adjustment-result.js +19 -0
  48. package/dist/interact/adjustment/adjustment-semantic.js +64 -0
  49. package/dist/interact/adjustment/adjustment-verification.js +35 -0
  50. package/dist/interact/adjustment/adjustment.js +52 -0
  51. package/dist/interact/adjustment/control-geometry.js +87 -0
  52. package/dist/interact/adjustment/control-values.js +136 -0
  53. package/dist/interact/adjustment/slider-labels.js +84 -0
  54. package/dist/interact/assertions/screen-assertion.js +37 -0
  55. package/dist/interact/assertions/state-assertion-context.js +35 -0
  56. package/dist/interact/assertions/state-assertion.js +76 -0
  57. package/dist/interact/assertions/state-comparison.js +141 -0
  58. package/dist/interact/assertions/visibility-assertion.js +34 -0
  59. package/dist/interact/conditional-action.js +12 -7
  60. package/dist/interact/index.js +77 -3092
  61. package/dist/interact/input-service.js +1 -0
  62. package/dist/interact/interaction-trace.js +21 -0
  63. package/dist/interact/model.js +1 -0
  64. package/dist/interact/observation.js +1 -0
  65. package/dist/interact/targeting/candidate-ancestor.js +58 -0
  66. package/dist/interact/targeting/candidate-result.js +23 -0
  67. package/dist/interact/targeting/candidate-scoring.js +50 -0
  68. package/dist/interact/targeting/element-helpers.js +216 -0
  69. package/dist/interact/targeting/find-element.js +65 -0
  70. package/dist/interact/targeting/nearby-control.js +56 -0
  71. package/dist/interact/targeting/resolved-elements.js +21 -0
  72. package/dist/interact/targeting/tap-target.js +17 -0
  73. package/dist/interact/targeting/target-scope.js +104 -0
  74. package/dist/interact/waits/legacy-log-wait.js +38 -0
  75. package/dist/interact/waits/legacy-wait-observations.js +103 -0
  76. package/dist/interact/waits/legacy-wait-state.js +34 -0
  77. package/dist/interact/waits/legacy-wait.js +48 -0
  78. package/dist/interact/waits/screen-wait.js +49 -0
  79. package/dist/interact/waits/ui-change-helpers.js +157 -0
  80. package/dist/interact/waits/ui-change-state.js +18 -0
  81. package/dist/interact/waits/ui-change-wait.js +77 -0
  82. package/dist/interact/waits/ui-wait-condition.js +56 -0
  83. package/dist/interact/waits/ui-wait-state.js +38 -0
  84. package/dist/interact/waits/ui-wait.js +106 -0
  85. package/dist/manage/android.js +55 -51
  86. package/dist/manage/index.js +38 -75
  87. package/dist/manage/ios/build-types.js +1 -0
  88. package/dist/manage/ios/build.js +210 -0
  89. package/dist/manage/ios/project-discovery.js +29 -0
  90. package/dist/manage/ios/scheme.js +18 -0
  91. package/dist/manage/ios.js +44 -256
  92. package/dist/observe/android/context.js +1 -0
  93. package/dist/observe/android/current-screen.js +55 -0
  94. package/dist/observe/android/fingerprint.js +13 -0
  95. package/dist/observe/android/log-stream.js +132 -0
  96. package/dist/observe/android/logs.js +107 -0
  97. package/dist/observe/android/screenshot.js +70 -0
  98. package/dist/observe/android/ui-tree.js +107 -0
  99. package/dist/observe/android.js +16 -520
  100. package/dist/observe/evidence.js +114 -75
  101. package/dist/observe/index.js +16 -263
  102. package/dist/observe/ios/context.js +16 -0
  103. package/dist/observe/ios/fingerprint.js +12 -0
  104. package/dist/observe/ios/log-stream.js +105 -0
  105. package/dist/observe/ios/logs.js +168 -0
  106. package/dist/observe/ios/screenshot.js +22 -0
  107. package/dist/observe/ios/ui-elements.js +241 -0
  108. package/dist/observe/ios/ui-tree.js +137 -0
  109. package/dist/observe/ios-trace/artifact.js +63 -0
  110. package/dist/observe/ios-trace/capabilities.js +86 -0
  111. package/dist/observe/ios-trace/native.js +93 -0
  112. package/dist/observe/ios-trace/notification.js +35 -0
  113. package/dist/observe/ios-trace/process.js +98 -0
  114. package/dist/observe/ios-trace/sessions.js +244 -0
  115. package/dist/observe/ios.js +17 -715
  116. package/dist/observe/logs/model.js +1 -0
  117. package/dist/{network → observe/network}/index.js +2 -2
  118. package/dist/observe/readiness-service.js +5 -3
  119. package/dist/observe/readiness.js +33 -14
  120. package/dist/observe/screenshot-encoding.js +41 -0
  121. package/dist/observe/snapshot-collection.js +79 -0
  122. package/dist/observe/snapshot-logs.js +30 -0
  123. package/dist/observe/snapshot-metadata.js +43 -31
  124. package/dist/observe/snapshot-semantic.js +115 -0
  125. package/dist/observe/system-trace-error.js +7 -0
  126. package/dist/observe/system-trace-router.js +81 -0
  127. package/dist/observe/system-trace.js +26 -21
  128. package/dist/runtime/action-results.js +270 -0
  129. package/dist/runtime/clock.js +4 -0
  130. package/dist/server/common.js +1 -256
  131. package/dist/server/definitions/assert.js +162 -0
  132. package/dist/server/definitions/diagnostic.js +118 -0
  133. package/dist/server/definitions/input.js +274 -0
  134. package/dist/server/definitions/manage.js +194 -0
  135. package/dist/server/definitions/observe.js +149 -0
  136. package/dist/server/definitions/target.js +138 -0
  137. package/dist/server/definitions/wait.js +105 -0
  138. package/dist/server/handlers/action-execution.js +163 -0
  139. package/dist/server/handlers/correlation.js +23 -0
  140. package/dist/server/handlers/debugger-handlers.js +67 -0
  141. package/dist/server/handlers/debugger-response.js +9 -0
  142. package/dist/server/handlers/diagnostic.js +77 -0
  143. package/dist/server/handlers/handler-types.js +1 -0
  144. package/dist/server/handlers/input.js +154 -0
  145. package/dist/server/handlers/interact.js +113 -0
  146. package/dist/server/handlers/journey-assertions.js +35 -0
  147. package/dist/server/handlers/journey.js +147 -0
  148. package/dist/server/handlers/manage.js +119 -0
  149. package/dist/server/handlers/observe.js +95 -0
  150. package/dist/server/handlers/payload.js +4 -0
  151. package/dist/server/handlers/tool-handlers.js +170 -0
  152. package/dist/server/schemas/condition-schema.js +2 -0
  153. package/dist/server/{debugger-schema.js → schemas/debugger-schema.js} +20 -12
  154. package/dist/server/{journey-schema.js → schemas/journey-schema.js} +7 -22
  155. package/dist/server/schemas/system-trace-schema.js +22 -0
  156. package/dist/server/tool-definitions.js +58 -1135
  157. package/dist/server-core.js +2 -2
  158. package/dist/server.js +4 -4
  159. package/dist/system/android-status.js +101 -0
  160. package/dist/system/android.js +37 -27
  161. package/dist/system/index.js +13 -439
  162. package/dist/system/ios-status.js +132 -0
  163. package/dist/system/ios.js +11 -8
  164. package/dist/system/model.js +1 -0
  165. package/dist/system/probes.js +105 -0
  166. package/dist/system/status-summary.js +43 -0
  167. package/dist/system/unsupported-status.js +52 -0
  168. package/dist/utils/android/commands.js +89 -0
  169. package/dist/utils/android/gradle.js +139 -0
  170. package/dist/utils/android/logs.js +103 -0
  171. package/dist/utils/android/ui-tree.js +233 -0
  172. package/dist/utils/android/utils.js +6 -564
  173. package/dist/utils/cli/idb/idb-helper.js +4 -6
  174. package/dist/utils/cli/idb/install-idb.js +13 -9
  175. package/dist/utils/cli/ios/preflight-ios.js +42 -38
  176. package/dist/utils/ios/utils.js +48 -46
  177. package/dist/utils/java.js +65 -49
  178. package/dist/utils/resolve-device.js +4 -2
  179. package/dist/utils/ui/index.js +26 -26
  180. package/docs/CHANGELOG.md +67 -1
  181. package/docs/README.md +38 -0
  182. package/docs/agent-plugin-installation-verification.md +39 -0
  183. package/docs/benchmarks/README.md +18 -0
  184. package/docs/benchmarks/interaction-latency/README.md +14 -0
  185. package/docs/benchmarks/interaction-latency/final.md +4 -1
  186. package/docs/benchmarks/interaction-latency/ios-validation.md +2 -0
  187. package/docs/benchmarks/ui-tree-performance/README.md +24 -16
  188. package/docs/benchmarks/ui-tree-performance/comparison.md +11 -10
  189. package/docs/code-structure.md +120 -0
  190. package/docs/specs/001-state-verification/spec.md +16 -14
  191. package/docs/specs/002-richer-element-identity/spec.md +12 -10
  192. package/docs/specs/003-wait-and-synchronization-reliability/spec.md +12 -10
  193. package/docs/specs/004-action-verification-routing/spec.md +3 -3
  194. package/docs/specs/005-unified-action-execution-and-verification-model/spec.md +2 -0
  195. package/docs/specs/006-runtime-action-instrumentation-and-binding-layer/spec.md +2 -0
  196. package/docs/specs/007-actionability-resolution-and-executable-target-selection/spec.md +3 -1
  197. package/docs/specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md +3 -4
  198. package/docs/specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md +2 -0
  199. package/docs/specs/010-verification-stabilization-and-temporal-convergence/spec.md +2 -2
  200. package/docs/specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md +2 -2
  201. package/docs/specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md +2 -0
  202. package/docs/specs/012-action-trace-and-xecution-observability/spec.md +2 -3
  203. package/docs/specs/013-wait-and-synchronization-reliability/spec.md +19 -19
  204. package/docs/specs/014-actionability-resolution/spec.md +20 -18
  205. package/docs/specs/015-adjustable-control-support/spec.md +17 -15
  206. package/docs/specs/016-environment-tools/spec.md +20 -20
  207. package/docs/specs/018-interaction-latency-and-batched-journeys/spec.md +2 -0
  208. package/docs/specs/019-ui-tree-observation-performance/spec.md +3 -1
  209. package/docs/specs/020-conditional-actions/intent.md +5 -3
  210. package/docs/specs/020-conditional-actions/plan.md +8 -9
  211. package/docs/specs/020-conditional-actions/spec.md +4 -2
  212. package/docs/specs/021-app-readiness-signals/intent.md +4 -2
  213. package/docs/specs/021-app-readiness-signals/plan.md +7 -6
  214. package/docs/specs/021-app-readiness-signals/spec.md +3 -1
  215. package/docs/specs/022-correlated-evidence/intent.md +4 -4
  216. package/docs/specs/022-correlated-evidence/plan.md +10 -11
  217. package/docs/specs/022-correlated-evidence/spec.md +3 -1
  218. package/docs/specs/023-android-system-traces/intent.md +3 -2
  219. package/docs/specs/023-android-system-traces/plan.md +5 -7
  220. package/docs/specs/023-android-system-traces/spec.md +3 -1
  221. package/docs/specs/024-android-heap-dump-capture/intent.md +3 -1
  222. package/docs/specs/025-android-runtime-debugger/intent.md +4 -3
  223. package/docs/specs/025-android-runtime-debugger/plan.md +3 -1
  224. package/docs/specs/025-android-runtime-debugger/spec.md +4 -3
  225. package/docs/specs/026-code-structure-and-quality-gates/health-plan.md +88 -0
  226. package/docs/specs/026-code-structure-and-quality-gates/intent.md +47 -0
  227. package/docs/specs/026-code-structure-and-quality-gates/plan.md +191 -0
  228. package/docs/specs/026-code-structure-and-quality-gates/spec.md +138 -0
  229. package/docs/specs/026-code-structure-and-quality-gates/verification-final.json +200 -0
  230. package/docs/specs/026-code-structure-and-quality-gates/verification-thin-facade.json +154 -0
  231. package/docs/specs/026-code-structure-and-quality-gates/verification.json +136 -0
  232. package/docs/specs/027-debugger-failure-diagnostics/intent.md +43 -0
  233. package/docs/specs/027-debugger-failure-diagnostics/plan.md +86 -0
  234. package/docs/specs/027-debugger-failure-diagnostics/spec.md +133 -0
  235. package/docs/specs/028-compact-debugger-responses/intent.md +41 -0
  236. package/docs/specs/028-compact-debugger-responses/plan.md +112 -0
  237. package/docs/specs/028-compact-debugger-responses/spec.md +108 -0
  238. package/docs/specs/029-kotlin-locals-validation/report.md +119 -0
  239. package/docs/specs/030-android-coroutine-validation/evidence.json +475 -0
  240. package/docs/specs/030-android-coroutine-validation/intent.md +47 -0
  241. package/docs/specs/030-android-coroutine-validation/plan.md +147 -0
  242. package/docs/specs/030-android-coroutine-validation/report.md +240 -0
  243. package/docs/specs/030-android-coroutine-validation/spec.md +107 -0
  244. package/docs/specs/031-ios-system-traces/intent.md +38 -0
  245. package/docs/specs/031-ios-system-traces/investigation.md +92 -0
  246. package/docs/specs/031-ios-system-traces/late-run-failure-evidence.json +32 -0
  247. package/docs/specs/031-ios-system-traces/plan.md +138 -0
  248. package/docs/specs/031-ios-system-traces/pr102-monitor-evidence.json +22 -0
  249. package/docs/specs/031-ios-system-traces/simulator-evidence.json +47 -0
  250. package/docs/specs/031-ios-system-traces/spec.md +135 -0
  251. package/docs/specs/032-ios-runtime-debugger/evidence.json +176 -0
  252. package/docs/specs/032-ios-runtime-debugger/intent.md +38 -0
  253. package/docs/specs/032-ios-runtime-debugger/plan.md +95 -0
  254. package/docs/specs/032-ios-runtime-debugger/report.md +205 -0
  255. package/docs/specs/032-ios-runtime-debugger/spec.md +153 -0
  256. package/docs/tools/TOOLS.md +31 -18
  257. package/docs/tools/{evidence.md → correlated-evidence.md} +9 -1
  258. package/docs/tools/debugger.md +120 -4
  259. package/docs/tools/interact.md +42 -20
  260. package/docs/tools/manage.md +25 -44
  261. package/docs/tools/observe.md +55 -23
  262. package/docs/tools/system-traces.md +89 -14
  263. package/docs/tools/system.md +12 -5
  264. package/package.json +12 -8
  265. package/scripts/agent-plugin.mjs +55 -5
  266. package/scripts/architecture-graph.mjs +69 -0
  267. package/scripts/check-architecture.mjs +47 -0
  268. package/scripts/code-quality-baseline.json +1 -0
  269. package/scripts/code-quality.mjs +161 -0
  270. package/src/contracts/execution-controls.ts +26 -0
  271. package/src/contracts/selector.ts +17 -0
  272. package/src/contracts/target.ts +19 -0
  273. package/src/contracts/ui.ts +94 -0
  274. package/src/debugger/android.ts +3 -2
  275. package/src/debugger/diagnostics.ts +43 -0
  276. package/src/debugger/guard.ts +10 -5
  277. package/src/debugger/ios-worker.ts +49 -0
  278. package/src/debugger/ios.ts +45 -0
  279. package/{worker → src/debugger/java}/DebuggerWorker.java +9 -3
  280. package/src/debugger/lldb/breakpoints.py +51 -0
  281. package/src/debugger/lldb/inspection.py +109 -0
  282. package/src/debugger/lldb/worker.py +203 -0
  283. package/src/debugger/protocol.ts +37 -0
  284. package/src/debugger/sessions.ts +326 -155
  285. package/src/debugger/worker.ts +57 -41
  286. package/src/interact/adjustment/adjustment-context.ts +83 -0
  287. package/src/interact/adjustment/adjustment-gestures.ts +48 -0
  288. package/src/interact/adjustment/adjustment-plan.ts +23 -0
  289. package/src/interact/adjustment/adjustment-resolution.ts +104 -0
  290. package/src/interact/adjustment/adjustment-result.ts +34 -0
  291. package/src/interact/adjustment/adjustment-semantic.ts +53 -0
  292. package/src/interact/adjustment/adjustment-verification.ts +60 -0
  293. package/src/interact/adjustment/adjustment.ts +49 -0
  294. package/src/interact/adjustment/control-geometry.ts +120 -0
  295. package/src/interact/adjustment/control-values.ts +141 -0
  296. package/src/interact/adjustment/slider-labels.ts +84 -0
  297. package/src/interact/assertions/screen-assertion.ts +39 -0
  298. package/src/interact/assertions/state-assertion-context.ts +48 -0
  299. package/src/interact/assertions/state-assertion.ts +64 -0
  300. package/src/interact/assertions/state-comparison.ts +168 -0
  301. package/src/interact/assertions/visibility-assertion.ts +41 -0
  302. package/src/interact/conditional-action.ts +18 -12
  303. package/src/interact/index.ts +89 -3494
  304. package/src/interact/input-service.ts +11 -0
  305. package/src/interact/interaction-trace.ts +36 -0
  306. package/src/interact/model.ts +109 -0
  307. package/src/interact/observation.ts +7 -0
  308. package/src/interact/targeting/candidate-ancestor.ts +49 -0
  309. package/src/interact/targeting/candidate-result.ts +24 -0
  310. package/src/interact/targeting/candidate-scoring.ts +42 -0
  311. package/src/interact/targeting/element-helpers.ts +243 -0
  312. package/src/interact/targeting/find-element.ts +47 -0
  313. package/src/interact/targeting/nearby-control.ts +46 -0
  314. package/src/interact/targeting/resolved-elements.ts +20 -0
  315. package/src/interact/targeting/tap-target.ts +22 -0
  316. package/src/interact/targeting/target-scope.ts +121 -0
  317. package/src/interact/waits/legacy-log-wait.ts +30 -0
  318. package/src/interact/waits/legacy-wait-observations.ts +55 -0
  319. package/src/interact/waits/legacy-wait-state.ts +48 -0
  320. package/src/interact/waits/legacy-wait.ts +34 -0
  321. package/src/interact/waits/screen-wait.ts +37 -0
  322. package/src/interact/waits/ui-change-helpers.ts +184 -0
  323. package/src/interact/waits/ui-change-state.ts +26 -0
  324. package/src/interact/waits/ui-change-wait.ts +59 -0
  325. package/src/interact/waits/ui-wait-condition.ts +52 -0
  326. package/src/interact/waits/ui-wait-state.ts +55 -0
  327. package/src/interact/waits/ui-wait.ts +84 -0
  328. package/src/manage/android.ts +56 -50
  329. package/src/manage/index.ts +63 -93
  330. package/src/manage/ios/build-types.ts +14 -0
  331. package/src/manage/ios/build.ts +230 -0
  332. package/src/manage/ios/project-discovery.ts +26 -0
  333. package/src/manage/ios/scheme.ts +16 -0
  334. package/src/manage/ios.ts +67 -298
  335. package/src/observe/android/context.ts +1 -0
  336. package/src/observe/android/current-screen.ts +63 -0
  337. package/src/observe/android/fingerprint.ts +13 -0
  338. package/src/observe/android/log-stream.ts +127 -0
  339. package/src/observe/android/logs.ts +110 -0
  340. package/src/observe/android/screenshot.ts +79 -0
  341. package/src/observe/android/ui-tree.ts +113 -0
  342. package/src/observe/android.ts +17 -531
  343. package/src/observe/evidence.ts +122 -70
  344. package/src/observe/index.ts +24 -286
  345. package/src/observe/ios/context.ts +22 -0
  346. package/src/observe/ios/fingerprint.ts +12 -0
  347. package/src/observe/ios/log-stream.ts +103 -0
  348. package/src/observe/ios/logs.ts +179 -0
  349. package/src/observe/ios/screenshot.ts +26 -0
  350. package/src/observe/ios/ui-elements.ts +266 -0
  351. package/src/observe/ios/ui-tree.ts +143 -0
  352. package/src/observe/ios-trace/artifact.ts +55 -0
  353. package/src/observe/ios-trace/capabilities.ts +74 -0
  354. package/src/observe/ios-trace/native.ts +72 -0
  355. package/src/observe/ios-trace/notification.ts +30 -0
  356. package/src/observe/ios-trace/process.ts +82 -0
  357. package/src/observe/ios-trace/sessions.ts +204 -0
  358. package/src/observe/ios.ts +18 -749
  359. package/src/observe/logs/model.ts +11 -0
  360. package/src/{network → observe/network}/index.ts +2 -2
  361. package/src/observe/readiness-service.ts +6 -4
  362. package/src/observe/readiness.ts +42 -21
  363. package/src/observe/screenshot-encoding.ts +32 -0
  364. package/src/observe/snapshot-collection.ts +69 -0
  365. package/src/observe/snapshot-logs.ts +26 -0
  366. package/src/observe/snapshot-metadata.ts +51 -31
  367. package/src/observe/snapshot-semantic.ts +153 -0
  368. package/src/observe/system-trace-error.ts +3 -0
  369. package/src/observe/system-trace-router.ts +61 -0
  370. package/src/observe/system-trace.ts +28 -19
  371. package/src/runtime/action-results.ts +376 -0
  372. package/src/runtime/clock.ts +9 -0
  373. package/src/server/common.ts +9 -359
  374. package/src/server/definitions/assert.ts +164 -0
  375. package/src/server/definitions/diagnostic.ts +120 -0
  376. package/src/server/definitions/input.ts +276 -0
  377. package/src/server/definitions/manage.ts +196 -0
  378. package/src/server/definitions/observe.ts +151 -0
  379. package/src/server/definitions/target.ts +140 -0
  380. package/src/server/definitions/wait.ts +107 -0
  381. package/src/server/handlers/action-execution.ts +164 -0
  382. package/src/server/handlers/correlation.ts +20 -0
  383. package/src/server/handlers/debugger-handlers.ts +58 -0
  384. package/src/server/handlers/debugger-response.ts +7 -0
  385. package/src/server/handlers/diagnostic.ts +86 -0
  386. package/src/server/handlers/handler-types.ts +7 -0
  387. package/src/server/handlers/input.ts +173 -0
  388. package/src/server/handlers/interact.ts +135 -0
  389. package/src/server/handlers/journey-assertions.ts +39 -0
  390. package/src/server/handlers/journey.ts +132 -0
  391. package/src/server/handlers/manage.ts +141 -0
  392. package/src/server/handlers/observe.ts +116 -0
  393. package/src/server/handlers/payload.ts +36 -0
  394. package/src/server/handlers/tool-handlers.ts +170 -0
  395. package/src/server/schemas/condition-schema.ts +2 -0
  396. package/src/server/schemas/debugger-schema.ts +46 -0
  397. package/src/server/{journey-schema.ts → schemas/journey-schema.ts} +7 -22
  398. package/src/server/schemas/system-trace-schema.ts +23 -0
  399. package/src/server/tool-definitions.ts +67 -1143
  400. package/src/server-core.ts +2 -2
  401. package/src/server.ts +4 -4
  402. package/src/system/android-status.ts +124 -0
  403. package/src/system/android.ts +33 -21
  404. package/src/system/index.ts +21 -525
  405. package/src/system/ios-status.ts +147 -0
  406. package/src/system/ios.ts +14 -10
  407. package/src/system/model.ts +41 -0
  408. package/src/system/probes.ts +114 -0
  409. package/src/system/status-summary.ts +47 -0
  410. package/src/system/unsupported-status.ts +55 -0
  411. package/src/types.ts +16 -106
  412. package/src/utils/android/commands.ts +81 -0
  413. package/src/utils/android/gradle.ts +132 -0
  414. package/src/utils/android/logs.ts +103 -0
  415. package/src/utils/android/ui-tree.ts +239 -0
  416. package/src/utils/android/utils.ts +16 -556
  417. package/src/utils/cli/idb/check-idb.ts +4 -4
  418. package/src/utils/cli/idb/idb-helper.ts +6 -8
  419. package/src/utils/cli/idb/install-idb.ts +15 -9
  420. package/src/utils/cli/ios/preflight-ios.ts +39 -31
  421. package/src/utils/cli/ios/run-ios-ui-tree-tap.ts +4 -4
  422. package/src/utils/ios/utils.ts +60 -61
  423. package/src/utils/java.ts +62 -43
  424. package/src/utils/resolve-device.ts +9 -6
  425. package/src/utils/ui/index.ts +28 -28
  426. package/test/device/automated/debugger/coroutine-lifecycle.ts +27 -0
  427. package/test/device/automated/debugger/coroutine-support.ts +140 -0
  428. package/test/device/automated/debugger/coroutine.android.test.ts +169 -0
  429. package/test/device/automated/debugger/kotlin.android.test.ts +127 -0
  430. package/test/device/automated/debugger/runtime.android.test.ts +19 -3
  431. package/test/device/automated/debugger/runtime.ios.test.ts +221 -0
  432. package/test/device/automated/interact/interaction.android.test.ts +56 -0
  433. package/test/device/automated/observe/ios-system-trace.smoke.ts +119 -0
  434. package/test/device/automated/observe/system-trace.smoke.ts +1 -1
  435. package/test/device/manual/interact/modul8_selector_tap.manual.ts +1 -1
  436. package/test/fixtures/contracts/tool-definitions.json +3696 -0
  437. package/test/fixtures/coroutine-app/MainActivity.kt +100 -0
  438. package/test/fixtures/coroutine-app/README.md +48 -0
  439. package/test/fixtures/coroutine-app/build.ts +35 -0
  440. package/test/fixtures/debugger-app/MainActivity.java +21 -0
  441. package/test/fixtures/debugger-app/MainActivity.kt +32 -0
  442. package/test/fixtures/debugger-app/README.md +52 -0
  443. package/test/fixtures/debugger-app/build.ts +25 -15
  444. package/test/fixtures/ios-debugger-app/Main.swift +95 -0
  445. package/test/fixtures/ios-debugger-app/README.md +27 -0
  446. package/test/fixtures/ios-debugger-app/build.ts +21 -0
  447. package/test/fixtures/latency-journey-app/README.md +2 -0
  448. package/test/fixtures/readiness-app/README.md +2 -0
  449. package/test/helpers/ios-trace-shutdown.ts +29 -0
  450. package/test/unit/debugger/coroutine-lifecycle.test.ts +43 -0
  451. package/test/unit/debugger/diagnostic-sessions.test.ts +67 -0
  452. package/test/unit/debugger/diagnostics.test.ts +80 -0
  453. package/test/unit/debugger/guards.test.ts +4 -0
  454. package/test/unit/debugger/handle-guards.test.ts +88 -0
  455. package/test/unit/debugger/ios-preparation.test.ts +57 -0
  456. package/test/unit/debugger/ios.test.ts +104 -0
  457. package/test/unit/debugger/lldb-worker.py +166 -0
  458. package/test/unit/debugger/lldb-worker.test.ts +10 -0
  459. package/test/unit/debugger/preparation.test.ts +32 -0
  460. package/test/unit/debugger/protocol.test.ts +15 -0
  461. package/test/unit/debugger/sessions.test.ts +1 -1
  462. package/test/unit/interact/adjustment_service.test.ts +42 -0
  463. package/test/unit/interact/adjustment_verification.test.ts +35 -0
  464. package/test/unit/interact/clock_dependencies.test.ts +63 -0
  465. package/test/unit/interact/element_helpers.test.ts +19 -0
  466. package/test/unit/interact/legacy_wait_service.test.ts +17 -0
  467. package/test/unit/interact/state_comparison.test.ts +20 -0
  468. package/test/unit/interact/target_scope.test.ts +35 -0
  469. package/test/unit/network/get_network_activity.test.ts +1 -1
  470. package/test/unit/observe/ios-trace-native.test.ts +90 -0
  471. package/test/unit/observe/ios-trace-sessions.test.ts +131 -0
  472. package/test/unit/observe/readiness.test.ts +1 -1
  473. package/test/unit/observe/screenshot-encoding.test.ts +30 -0
  474. package/test/unit/observe/snapshot-logs.test.ts +14 -0
  475. package/test/unit/observe/system-trace-router.test.ts +49 -0
  476. package/test/unit/quality/agent-plugin-export.test.ts +38 -0
  477. package/test/unit/quality/architecture.test.ts +60 -0
  478. package/test/unit/quality/code-quality.test.ts +58 -0
  479. package/test/unit/quality/interaction_boundaries.test.ts +24 -0
  480. package/test/unit/server/conditional_actions.test.ts +1 -1
  481. package/test/unit/server/conditional_schema.test.ts +2 -2
  482. package/test/unit/server/debugger-response.test.ts +63 -0
  483. package/test/unit/server/system-trace.test.ts +12 -3
  484. package/test/unit/server/tool_definitions_parity.test.ts +8 -0
  485. package/dist/server/debugger-handlers.js +0 -38
  486. package/dist/server/system-trace-schema.js +0 -17
  487. package/dist/server/tool-handlers.js +0 -1000
  488. package/docs/ROADMAP.md +0 -646
  489. package/docs/benchmarks/ui-tree-performance/bridge-probe.json +0 -29
  490. package/docs/benchmarks/ui-tree-performance/initial-baseline.json +0 -59560
  491. package/docs/specs/baseline-spec-v0.md +0 -312
  492. package/docs/specs/mcp-tooling-spec-v1.md +0 -446
  493. package/src/server/debugger-handlers.ts +0 -29
  494. package/src/server/debugger-schema.ts +0 -37
  495. package/src/server/system-trace-schema.ts +0 -17
  496. package/src/server/tool-handlers.ts +0 -1008
  497. /package/dist/{server/condition-schema.js → contracts/condition.js} +0 -0
  498. /package/dist/server/{readiness-schema.js → schemas/readiness-schema.js} +0 -0
  499. /package/src/{server/condition-schema.ts → contracts/condition.ts} +0 -0
  500. /package/src/server/{readiness-schema.ts → schemas/readiness-schema.ts} +0 -0
@@ -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,90 @@
1
+ import assert from 'node:assert/strict'
2
+ import { promises as fs } from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { iosCapabilities, resolveIosApp } from '../../../src/observe/ios-trace/capabilities.js'
6
+ import { validateArtifact, validateToc, traceSize, IOS_TRACE_MAX_BYTES } from '../../../src/observe/ios-trace/artifact.js'
7
+ import { IosTraceNative } from '../../../src/observe/ios-trace/native.js'
8
+ import { spawnTrace, stopTraceChild, classifyTraceFailure, type TraceSpawn, type TraceCommandResult } from '../../../src/observe/ios-trace/process.js'
9
+ const app={app_id:'fixture',pid:123,executable:'/Fixture.app/Fixture',started_at:'Wed Nov 4 10:00:00 2026'}
10
+ const toc=(id='sim',pid=123,template='Time Profiler')=>`<trace-toc><run><info><target><device uuid="${id}"/><process pid="${pid}"/></target><summary><template-name>${template}</template-name></summary></info><processes><process pid="${pid}" path="/Fixture.app/Fixture"/></processes><data><table schema="time-sample"/></data></run></trace-toc>`
11
+ function fake(){
12
+ const calls:{command:string;args:string[];signals:NodeJS.Signals[]}[]=[],lines:string[]=[]
13
+ let mismatch=false,failExport=false,failReady=false
14
+ const spawn:TraceSpawn=(command,args)=>{
15
+ const call={command,args,signals:[] as NodeJS.Signals[]};calls.push(call)
16
+ const recording=args.includes('record'),listener=command.endsWith('/notification'),pending=recording||listener
17
+ let alive=pending,resolve!:(r:TraceCommandResult)=>void
18
+ const done=new Promise<TraceCommandResult>(r=>{resolve=r})
19
+ const execute=async()=>{
20
+ let output=''
21
+ if(args.includes('get_app_container'))output='/Fixture.app'
22
+ else if(args.includes('launchctl'))output='123 0 UIKitApplication:fixture[abc]'
23
+ else if(command==='/bin/ps')output=args.includes('lstart=')?app.started_at:mismatch?'/Other.app/Other':app.executable
24
+ else if(args.includes('clang'))await fs.writeFile(args[args.indexOf('-o')+1],'binary')
25
+ else if(args.includes('xcodebuild'))output='Xcode 27.2'
26
+ else if(args.includes('templates'))output='== Standard Templates ==\nTime Profiler\nSystem Trace\nAnimation Hitches'
27
+ else if(args.includes('xctrace')&&args.includes('devices'))output='fixture (sim)'
28
+ else if(args.includes('devices'))output=JSON.stringify({devices:{'iOS-26-4':[{udid:'sim',state:'Booted',name:'fixture',isAvailable:true}]}})
29
+ else if(args.includes('export')){
30
+ if(failExport){resolve({code:2,output:'private stderr',timedOut:false});return}
31
+ const file=args[args.indexOf('--output')+1]
32
+ await fs.writeFile(file,args.includes('--toc')?toc():'<trace-query-result><node><schema name="time-sample"/><row><sample-time>1</sample-time></row></node></trace-query-result>')
33
+ }
34
+ if(!pending)resolve({code:0,output,timedOut:false})
35
+ }
36
+ void execute()
37
+ return {done,alive:()=>alive,signal:s=>{call.signals.push(s);alive=false;resolve({code:0,output:'Stopped',timedOut:false})},line:async(line)=>{lines.push(line);if(failReady&&line==='READY')throw new Error('READINESS_TIMEOUT')}}
38
+ }
39
+ return {spawn,calls,lines,flags:(v:{mismatch?:boolean;export?:boolean;ready?:boolean})=>{mismatch=v.mismatch??false;failExport=v.export??false;failReady=v.ready??false}}
40
+ }
41
+ async function run(){
42
+ const f=fake(),directory=await fs.mkdtemp(path.join(os.tmpdir(),'ios-trace-unit-'))
43
+ try{
44
+ assert.deepEqual(await resolveIosApp('sim','fixture',f.spawn),app)
45
+ f.flags({mismatch:true});await assert.rejects(()=>resolveIosApp('sim','fixture',f.spawn),/APP_IDENTITY_UNAVAILABLE/);f.flags({})
46
+ const cap=await iosCapabilities('sim','fixture','Time Profiler',f.spawn,'darwin')
47
+ assert.equal(cap.supported,true);assert.equal(cap.prerequisites.permissions,'unknown');assert.equal(cap.templates.filter(t=>t.supported).length,1)
48
+ assert.equal((await iosCapabilities('sim',undefined,'System Trace',f.spawn,'darwin')).supported,false)
49
+ assert.equal((await iosCapabilities('physical',undefined,'Time Profiler',f.spawn,'darwin')).supported,false)
50
+ assert.equal((await iosCapabilities('sim',undefined,'Time Profiler',f.spawn,'linux')).reasons[0],'MACOS_REQUIRED')
51
+ assert.ok(!f.calls.some(c=>c.args.includes('record')),'Discovery must not record')
52
+ validateToc(toc(),'sim',app)
53
+ assert.throws(()=>validateToc(toc(),'sim',{...app,executable:'/Other.app/Other'}),/ARTIFACT_APP_MISMATCH/)
54
+ assert.equal(classifyTraceFailure({code:1,output:'secret path: permission denied',timedOut:false}),'PROFILING_PERMISSION_REQUIRED')
55
+ assert.equal(classifyTraceFailure({code:1,output:'Cannot find process for provided pid: 123',timedOut:false}),'APP_NOT_RUNNING')
56
+ for(const xml of [toc('other'),toc('sim',456),toc('sim',123,'System Trace'),'<invalid>'])assert.throws(()=>validateToc(xml,'sim',app))
57
+ const file=path.join(directory,'capture.trace');await fs.mkdir(file);await fs.writeFile(path.join(file,'data'),'samples')
58
+ const artifact=await validateArtifact(file,'sim',app,f.spawn)
59
+ assert.equal(artifact.validation.sample_rows,1);assert.equal(artifact.kind,'directory');assert.equal((await fs.stat(path.join(file,'data'))).mode&0o777,0o600)
60
+ await assert.rejects(()=>fs.stat(path.join(directory,'samples.xml')),{code:'ENOENT'})
61
+ f.flags({export:true});await assert.rejects(()=>validateArtifact(file,'sim',app,f.spawn));f.flags({})
62
+ const outside=path.join(directory,'link');await fs.symlink(file,outside);await assert.rejects(()=>traceSize(outside),/UNSAFE_ARTIFACT/)
63
+ const huge=path.join(directory,'huge');await fs.writeFile(huge,'');await fs.truncate(huge,IOS_TRACE_MAX_BYTES+1)
64
+ assert.ok(await traceSize(huge)>IOS_TRACE_MAX_BYTES)
65
+ const native=new IosTraceNative(f.spawn)
66
+ const recording=await native.start('sim',app,directory,30000,()=>{})
67
+ assert.deepEqual(f.lines,['ARMED','READY'])
68
+ const recorder=f.calls.find(c=>c.args.includes('record'))!,listener=f.calls.find(c=>c.command.endsWith('/notification'))!
69
+ assert.equal(recorder.args[recorder.args.indexOf('--device')+1],'sim')
70
+ assert.equal(recorder.args[recorder.args.indexOf('--attach')+1],'123')
71
+ assert.equal(recorder.args[recorder.args.indexOf('--notify-tracing-started')+1],listener.args[0])
72
+ assert.ok(!recorder.args.includes('--no-prompt'));await native.stop(recording);assert.deepEqual(recorder.signals,['SIGINT'])
73
+ const failed=fake();failed.flags({ready:true});const other=new IosTraceNative(failed.spawn)
74
+ await assert.rejects(()=>other.start('sim',app,directory,30000,()=>{}),/READINESS_TIMEOUT/)
75
+ assert.ok(failed.calls.find(c=>c.args.includes('record'))?.signals.includes('SIGINT'))
76
+ native.shutdown();other.shutdown()
77
+ const child=spawnTrace(process.execPath,['-e','process.stdout.write("READY\\n");setInterval(()=>{},1000)'],2000)
78
+ await child.line('READY',1000);await stopTraceChild(child,1000);assert.equal(child.alive(),false)
79
+ const signals:NodeJS.Signals[]=[]
80
+ const stuck={done:new Promise<TraceCommandResult>(()=>{}),alive:()=>true,line:async()=>{},signal:(s:NodeJS.Signals)=>{signals.push(s)}}
81
+ await assert.rejects(()=>stopTraceChild(stuck,1),/ACTION_TIMEOUT/)
82
+ assert.deepEqual(signals,['SIGINT','SIGKILL'])
83
+ const noisy=await spawnTrace(process.execPath,['-e','process.stdout.write("x".repeat(100000))'],2000).done
84
+ assert.equal(noisy.output.length,65536)
85
+ const timeout=await spawnTrace(process.execPath,['-e','setInterval(()=>{},1000)'],30).done
86
+ assert.equal(timeout.timedOut,true)
87
+ console.log('PASS iOS native discovery, notification, ownership, artifact schemas and bounded children')
88
+ }finally{await fs.rm(directory,{recursive:true,force:true})}
89
+ }
90
+ run().catch(error=>{console.error(error);process.exitCode=1})
@@ -0,0 +1,131 @@
1
+ import assert from 'node:assert/strict'
2
+ import { promises as fs } from 'node:fs'
3
+ import path from 'node:path'
4
+ import { IosTraceSessions } from '../../../src/observe/ios-trace/sessions.js'
5
+ import { IosTraceNative, type IosRecording } from '../../../src/observe/ios-trace/native.js'
6
+ import type { TraceClock } from '../../../src/observe/system-trace.js'
7
+ import type { TraceCommandResult } from '../../../src/observe/ios-trace/process.js'
8
+
9
+ const target = { platform: 'ios' as const, id: 'sim', simulator: true, osVersion: '26.4', model: 'fixture' }
10
+ function fixture() {
11
+ let monitorTick = 0, exitOnMonitor = false, exportMessage = 'EXPORT_FAILED'
12
+ let now = 1000, failStart = false, failStop = false, failExport = false, monitor = '', stops = 0, starts = 0, validations = 0
13
+ const timers = new Map<ReturnType<typeof setTimeout>, () => void>(), recordings: IosRecording[] = [], directories: string[] = []
14
+ const time: TraceClock = { wall: () => now, mono: () => now, sleep: async ms => {now += ms},
15
+ schedule: fn => { const id = {} as ReturnType<typeof setTimeout>; timers.set(id,fn); return id }, clear: id => {timers.delete(id)} }
16
+ class Native extends IosTraceNative {
17
+ resolve = async () => ({ app_id:'fixture', pid:100, executable:'/fixture.app/Fixture',started_at:'fixture-start' })
18
+ async start(_d: string, _app: unknown, directory: string, _duration: number, own: (r: IosRecording) => void) {
19
+ starts++; directories.push(directory)
20
+ let alive = true, resolve!: (v: TraceCommandResult) => void
21
+ const done = new Promise<TraceCommandResult>(r => {resolve=r})
22
+ const recording: IosRecording = {file:path.join(directory,'capture.trace'), ready_at_ms:now,
23
+ recorder:{done, alive:()=>alive, line:async()=>{},signal:()=>{alive=false;resolve({code:0,output:'Reached specified time limit',timedOut:false})}}}
24
+ own(recording); recordings.push(recording)
25
+ await fs.mkdir(recording.file)
26
+ await fs.writeFile(path.join(recording.file,'data'),'sample')
27
+ if(failStart) throw new Error('READINESS_TIMEOUT')
28
+ return recording
29
+ }
30
+ async stop(r: IosRecording) {stops++; if(failStop) throw new Error('ACTION_TIMEOUT');r.recorder.signal('SIGINT');return r.recorder.done}
31
+ async monitor(device: string, app: {pid:number;executable:string}, recording: IosRecording) {
32
+ assert.equal(device,target.id);assert.equal(app.pid,100);assert.equal(app.executable,'/fixture.app/Fixture')
33
+ assert.equal(recording,recordings.at(-1),'Retries must retain the same owned recording')
34
+ now+=monitorTick
35
+ if(exitOnMonitor)recording.recorder.signal('SIGINT')
36
+ if(monitor)throw new Error(monitor)
37
+ }
38
+ async validate(r: IosRecording) {validations++;if(failExport)throw new Error(exportMessage);return {path:r.file,format:'instruments' as const,kind:'directory' as const,size_bytes:6,validation:{readable:true,schema:'time-sample',sample_rows:1}}}
39
+ shutdown() {for(const r of recordings)r.recorder.signal('SIGINT')}
40
+ }
41
+ const sessions = new IosTraceSessions(new Native(),time)
42
+ return {sessions,recordings,timers,flags:(v:{start?:boolean;stop?:boolean;export?:boolean;monitor?:string;monitorTick?:number;exitOnMonitor?:boolean;exportMessage?:string})=>{failStart=v.start??false;failStop=v.stop??false;failExport=v.export??false;monitor=v.monitor??'';monitorTick=v.monitorTick??0;exitOnMonitor=v.exitOnMonitor??false;exportMessage=v.exportMessage??'EXPORT_FAILED'},
43
+ tick:(ms:number)=>{now+=ms},counts:()=>({stops,starts,validations}),
44
+ cleanup:async()=>{await sessions.shutdown();for(const d of directories)await fs.rm(d,{recursive:true,force:true})}}
45
+ }
46
+ async function run() {
47
+ const f=fixture()
48
+ try {
49
+ const start=await f.sessions.start(target,'fixture')
50
+ await assert.rejects(()=>f.sessions.start(target,'fixture'),/TRACE_BUSY/)
51
+ assert.equal(start.state,'recording');assert.equal(start.recording_ready_at_ms,1000)
52
+ const finish=await f.sessions.link(start.trace_id,target,'journey')
53
+ for(const device of [{platform:'android',id:'sim'},{platform:'ios',id:'other'}])await assert.rejects(()=>f.sessions.link(start.trace_id,device,'wrong'),/TRACE_NOT_FOUND/)
54
+ const [a,b]=await Promise.all([f.sessions.control(start.trace_id,'stop'),f.sessions.control(start.trace_id,'stop')])
55
+ assert.equal(a.state,'completed');assert.deepEqual(a,b);assert.deepEqual(a.correlation_ids,['journey']);assert.equal((await finish()).coverage,'partial')
56
+ assert.equal(f.counts().validations,1)
57
+ await assert.rejects(()=>f.sessions.link(start.trace_id,target,'stale'),/TRACE_NOT_FOUND/)
58
+ await assert.rejects(()=>f.sessions.control('foreign','stop'),/TRACE_NOT_FOUND/)
59
+ assert.equal((await f.sessions.control(start.trace_id,'cancel')).artifact?.path,a.artifact?.path)
60
+ const cancelled=await f.sessions.start(target,'fixture')
61
+ const dir=path.dirname(f.recordings[1].file)
62
+ assert.equal((await f.sessions.control(cancelled.trace_id,'cancel')).state,'cancelled')
63
+ await assert.rejects(()=>fs.stat(dir),{code:'ENOENT'})
64
+ const expires=await f.sessions.start(target,'fixture',1000);f.tick(1001)
65
+ const expired=await f.sessions.control(expires.trace_id,'status')
66
+ assert.equal(expired.state,'completed');assert.equal(expired.completion_reason,'duration_limit')
67
+ f.tick(600001);await assert.rejects(()=>f.sessions.control(start.trace_id,'status'),/TRACE_NOT_FOUND/)
68
+ }finally{await f.cleanup()}
69
+ for(const failure of ['start','stop','export','monitor'] as const){
70
+ const f=fixture()
71
+ try{
72
+ if(failure==='start')f.flags({start:true})
73
+ const s=await f.sessions.start(target,'fixture')
74
+ if(failure==='start'){assert.equal(s.state,'failed');assert.equal(s.reason,'READINESS_TIMEOUT');assert.equal(s.cleanup.status,'complete');assert.equal(f.recordings[0].recorder.alive(),false);continue}
75
+ f.flags(failure==='monitor'?{monitor:'STORAGE_LIMIT'}:{[failure]:true})
76
+ const failed=await f.sessions.control(s.trace_id,failure==='monitor'?'status':'stop')
77
+ assert.equal(failed.state,'failed');assert.equal(failed.artifact,null)
78
+ if(failure==='monitor'){assert.equal(failed.reason,'STORAGE_LIMIT');assert.equal(failed.cleanup.status,'complete');continue}
79
+ f.flags({})
80
+ assert.equal((await f.sessions.control(s.trace_id,'stop')).state,'completed')
81
+ assert.equal(f.counts().starts,1,'Retry must not start a new recording')
82
+ }finally{await f.cleanup()}
83
+ }
84
+ const exportClassification=fixture()
85
+ try {
86
+ const s=await exportClassification.sessions.start(target,'fixture')
87
+ exportClassification.flags({export:true,exportMessage:'COMMAND_FAILED'})
88
+ const failed=await exportClassification.sessions.control(s.trace_id,'stop')
89
+ assert.equal(failed.reason,'TRACE_ARTIFACT_FAILED','Monitor classification must not alter export errors')
90
+ assert.match(failed.recovery!,/Retry stop/)
91
+ }finally{await exportClassification.cleanup()}
92
+ for(const reason of ['COMMAND_FAILED','ACTION_TIMEOUT','TARGET_DISCONNECTED','private unexpected output']) {
93
+ const f=fixture()
94
+ try {
95
+ const s=await f.sessions.start(target,'fixture')
96
+ f.flags({monitor:reason})
97
+ for(let n=0;n<2;n++)assert.equal((await f.sessions.control(s.trace_id,'status')).state,'recording')
98
+ assert.equal(f.counts().stops,0)
99
+ f.flags({});assert.equal((await f.sessions.control(s.trace_id,'status')).state,'recording')
100
+ f.flags({monitor:reason})
101
+ for(let n=0;n<2;n++)assert.equal((await f.sessions.control(s.trace_id,'status')).state,'recording','Successful poll must reset consecutive failure count')
102
+ const failed=await f.sessions.control(s.trace_id,'status')
103
+ assert.equal(failed.state,'failed');assert.equal(failed.reason,reason==='private unexpected output'?'TRACE_MONITOR_FAILED':reason)
104
+ assert.equal(failed.cleanup.status,'complete');assert.equal(failed.artifact,null)
105
+ assert.equal(f.counts().starts,1);assert.equal(f.recordings[0].recorder.alive(),false)
106
+ assert.ok(!JSON.stringify(failed).includes('private unexpected output'))
107
+ }finally{await f.cleanup()}
108
+ }
109
+ for(const reason of ['APP_IDENTITY_CHANGED','APP_NOT_RUNNING']) {
110
+ const f=fixture()
111
+ try {
112
+ const s=await f.sessions.start(target,'fixture');f.flags({monitor:reason})
113
+ const failed=await f.sessions.control(s.trace_id,'status')
114
+ assert.equal(failed.state,'failed');assert.equal(failed.reason,reason);assert.equal(failed.cleanup.status,'complete')
115
+ }finally{await f.cleanup()}
116
+ }
117
+ for(const ended of ['deadline','recorder'] as const) {
118
+ const f=fixture()
119
+ try {
120
+ const s=await f.sessions.start(target,'fixture',1000)
121
+ f.flags({monitor:'COMMAND_FAILED',monitorTick:ended==='deadline'?1001:0,exitOnMonitor:ended==='recorder'})
122
+ const result=await f.sessions.control(s.trace_id,'status')
123
+ assert.equal(result.state,'completed','A tolerated failed poll must still reconcile duration/recorder completion')
124
+ assert.equal(result.completion_reason,'duration_limit');assert.ok(result.artifact)
125
+ }finally{await f.cleanup()}
126
+ }
127
+ const shutdown=fixture()
128
+ try{const s=await shutdown.sessions.start(target,'fixture');assert.deepEqual(await shutdown.sessions.shutdown(),[]);assert.equal((await shutdown.sessions.control(s.trace_id,'status')).state,'cancelled');await assert.rejects(()=>shutdown.sessions.start(target,'fixture'),/TRACE_BUSY/)}finally{await shutdown.cleanup()}
129
+ console.log('PASS iOS trace lifecycle, linkage, failure cleanup, retries and expiry')
130
+ }
131
+ run().catch(error=>{console.error(error);process.exitCode=1})
@@ -36,6 +36,6 @@ 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)
39
+ assert.equal(new ReadinessTracker('ios', 'fixture').summary.capabilities.debugger_paused, true)
40
40
  assert.ok(tracker.summary.limitations.includes('MANAGED_DEBUGGER_ONLY'))
41
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')
@@ -0,0 +1,49 @@
1
+ import assert from 'node:assert/strict'
2
+ import { SystemTraceRouter } from '../../../src/observe/system-trace-router.js'
3
+ import type { SystemTraceSessions, TraceResult } from '../../../src/observe/system-trace.js'
4
+ import type { IosTraceSessions, IosTraceResult } from '../../../src/observe/ios-trace/sessions.js'
5
+ import type { IosTraceNative } from '../../../src/observe/ios-trace/native.js'
6
+ function backend(platform:'android'|'ios') {
7
+ let next=0,starts=0
8
+ const results=new Map<string,TraceResult|IosTraceResult>()
9
+ const api={
10
+ capabilities:async()=>({}),
11
+ start:async(device:string|{id:string})=>{
12
+ starts++
13
+ const result={trace_id:platform+ ++next,device:{platform,id:typeof device==='string'?device:device.id},state:'recording',cleanup:{status:'pending'}} as TraceResult|IosTraceResult
14
+ results.set(result.trace_id,result);return result
15
+ },
16
+ control:async(id:string,op:string)=>{
17
+ const result=results.get(id);if(!result)throw new Error('TRACE_NOT_FOUND')
18
+ if(op==='cancel'){result.cleanup.status='complete';result.state='cancelled'}
19
+ return result
20
+ },
21
+ link:async()=>async()=>({trace_id:'linked',coverage:'covered',clock_alignment:'unknown'}),
22
+ shutdown:async()=>[]
23
+ }
24
+ return {api,results,count:()=>starts}
25
+ }
26
+ async function run(){
27
+ const android=backend('android'),ios=backend('ios')
28
+ let release!:()=>void,gate=Promise.resolve()
29
+ const native={shutdown:()=>{},capabilities:async(device:string)=>{await gate;return {supported:true,device:{platform:'ios',id:device,simulator:true}}}} as unknown as IosTraceNative
30
+ const router=new SystemTraceRouter(android.api as unknown as SystemTraceSessions,ios.api as unknown as IosTraceSessions,native)
31
+ const a=await router.start('a'),b=await router.start('b',30000,{appId:'fixture'})
32
+ await assert.rejects(()=>router.link(b.trace_id,{platform:'android',id:'b'},'bad'),/TRACE_NOT_FOUND/)
33
+ await assert.rejects(()=>router.link(b.trace_id,{platform:'ios',id:'other'},'bad'),/TRACE_NOT_FOUND/)
34
+ await assert.rejects(()=>router.start('b',30000,{appId:'fixture'}),/TRACE_BUSY/)
35
+ await router.start('c');await router.start('d',30000,{appId:'fixture'})
36
+ await assert.rejects(()=>router.start('fifth'),/TRACE_BUSY/)
37
+ await router.control(a.trace_id,'cancel')
38
+ await router.start('freed')
39
+ await router.control(b.trace_id,'cancel')
40
+ gate=new Promise<void>(r=>{release=r})
41
+ const first=router.start('pending',30000,{appId:'fixture'})
42
+ await new Promise(r=>setImmediate(r))
43
+ await assert.rejects(()=>router.start('pending',30000,{appId:'fixture'}),/TRACE_BUSY/)
44
+ release();await first
45
+ assert.equal(ios.count(),3)
46
+ await router.shutdown();await assert.rejects(()=>router.start('closed'),/TRACE_BUSY/)
47
+ console.log('PASS cross-platform trace routing, pinned linkage, shared reservations and shutdown')
48
+ }
49
+ run().catch(error=>{console.error(error);process.exitCode=1})