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,52 @@
1
+ import assert from 'node:assert/strict'
2
+ import { AndroidTraceNative } from '../../../src/observe/system-trace-android.js'
3
+ import type { execCmd } from '../../../src/utils/exec.js'
4
+
5
+ async function run() {
6
+ const calls: Array<{ args: string[]; timeout: number | undefined }> = []
7
+ let failure = '', processes = ''
8
+ const execute: typeof execCmd = async (_cmd, args, options = {}) => {
9
+ calls.push({ args, timeout: options.timeout })
10
+ assert.equal(args[0], '-s'); assert.equal(args[1], 'serial with spaces')
11
+ if (failure && args.join(' ').includes(failure)) return { exitCode: null, stdout: '', stderr: 'ACTION_TIMEOUT' }
12
+ let out = ''
13
+ if (args.includes('--help')) return { exitCode: 1, stdout: '', stderr: 'Usage: perfetto --background-wait --config' }
14
+ if (args.includes('getprop')) out = '36'
15
+ if (args.includes('--list_categories')) out = ' sched - Scheduling\n input - Input\n gfx - Graphics'
16
+ if (args.join(' ').includes('YQ==')) out = 'a'
17
+ if (args.join(' ').includes('for pid')) out = processes
18
+ if (args.includes('stat')) out = '1024'
19
+ return { exitCode: 0, stdout: out, stderr: '' }
20
+ }
21
+ const native = new AndroidTraceNative(execute)
22
+ const remote = '/data/misc/perfetto-traces/mcp-62a51510-c303-4a1b-af0e-f97cc976d740.perfetto-trace'
23
+ assert.equal((await native.capabilities('serial with spaces')).supported, true)
24
+ for (const probe of ['--help', '--query', 'getprop', '--list_categories', 'test -w', 'YQ==']) {
25
+ failure = probe
26
+ assert.equal((await native.capabilities('serial with spaces')).supported, false, probe)
27
+ }
28
+ failure = ''
29
+ await native.start('serial with spaces', remote, 1001, ['sched', 'input'])
30
+ assert.equal(calls.at(-1)!.timeout, 35000)
31
+ assert.ok(calls.at(-1)!.args.join(' ').includes('--background-wait -c -'))
32
+ assert.equal(await native.find('serial with spaces', remote), null)
33
+ processes = '123 456'
34
+ assert.deepEqual(await native.find('serial with spaces', remote), { pid: 123, startTime: '456' })
35
+ assert.ok(calls.at(-1)!.args.join(' ').includes('stat=$(cat "$p/stat" 2>/dev/null) || { [ ! -d "$p" ] && continue; exit 1; }'), 'Unreadable process identity must fail closed')
36
+ processes = '123 456\n999 888'
37
+ await assert.rejects(() => native.find('serial with spaces', remote), /IDENTITY_UNAVAILABLE/)
38
+ const previous = calls.length
39
+ await assert.rejects(() => native.stop('serial with spaces', remote, { pid: 0, startTime: '4' }))
40
+ await assert.rejects(() => native.start('serial with spaces', '/tmp/other; killall perfetto', 1000, []))
41
+ assert.equal(calls.length, previous)
42
+ await native.stop('serial with spaces', remote, { pid: 123, startTime: '456' })
43
+ const signal = calls.at(-1)!.args.join(' ')
44
+ assert.ok(signal.indexOf('awk') < signal.indexOf('kill -TERM'))
45
+ assert.ok(signal.indexOf('grep -Fx') < signal.indexOf('kill -TERM'))
46
+ assert.ok(!signal.includes('killall'))
47
+ failure = 'pull'
48
+ await assert.rejects(() => native.pull('serial with spaces', remote, '/tmp/fixture'), /ACTION_TIMEOUT/)
49
+ assert.equal(calls.at(-1)!.timeout, 30000)
50
+ console.log('Android Perfetto adapter probes, targeting, guards and deadlines: PASS')
51
+ }
52
+ run().catch(error => { console.error(error); process.exitCode = 1 })
@@ -0,0 +1,166 @@
1
+ import assert from 'node:assert/strict'
2
+ import { promises as fs } from 'node:fs'
3
+ import path from 'node:path'
4
+ import { SystemTraceSessions, type TraceClock } from '../../../src/observe/system-trace.js'
5
+ import { TRACE_MAX_BYTES, traceConfig, type TraceNative, type TraceCapabilities, type TraceProcess } from '../../../src/observe/system-trace-android.js'
6
+
7
+ function fixture() {
8
+ let now = 1000000
9
+ let failStart = '', failStop = '', failPull = '', failRemove = '', failFind = ''
10
+ let supported = true, empty = false, oversized = false, stuck = false
11
+ let starts = 0, stops = 0, pulls = 0, removes = 0
12
+ const processes = new Map<string, TraceProcess>()
13
+ const pending = new Map<any, () => void>()
14
+ const time: TraceClock = { wall: () => now, mono: () => now, sleep: async ms => { now += ms },
15
+ schedule(fn) { const id = {} as any; pending.set(id, fn); return id }, clear: id => { pending.delete(id) } }
16
+ const native: TraceNative = {
17
+ async capabilities(device) { return { supported, backend: 'perfetto', device: { platform: 'android', id: device }, prerequisites: {}, reasons: [], enabled_sources: ['sched', 'input'], unavailable_categories: [], limitations: [] } as TraceCapabilities },
18
+ async start(_d, remote) { starts++; processes.set(remote, { pid: starts + 100, startTime: String(starts) }); if (failStart) throw new Error(failStart) },
19
+ async find(_d, remote) { if (failFind) throw new Error(failFind); return processes.get(remote) ?? null },
20
+ async stop(_d, remote) { stops++; if (failStop) throw new Error(failStop); if (!stuck) processes.delete(remote) },
21
+ async pull(_d, _r, local) { pulls++; await fs.writeFile(local, empty ? '' : 'perfetto-fixture'); if (oversized) await fs.truncate(local, TRACE_MAX_BYTES + 1); if (failPull) throw new Error(failPull) },
22
+ async remove() { removes++; if (failRemove) throw new Error(failRemove) }
23
+ }
24
+ const sessions = new SystemTraceSessions(native, time)
25
+ const files: string[] = []
26
+ return { sessions, native, time, processes, pending, files,
27
+ tick(ms: number) { now += ms }, get counts() { return { starts, stops, pulls, removes } },
28
+ flags(v: { start?: string; stop?: string; pull?: string; remove?: string; find?: string; supported?: boolean; empty?: boolean; oversized?: boolean; stuck?: boolean }) {
29
+ failStart = v.start ?? ''; failStop = v.stop ?? ''; failPull = v.pull ?? ''; failRemove = v.remove ?? ''; failFind = v.find ?? ''
30
+ supported = v.supported ?? true; empty = v.empty ?? false; oversized = v.oversized ?? false; stuck = v.stuck ?? false
31
+ },
32
+ async cleanup() { await sessions.shutdown(); await Promise.all(files.map(file => fs.rm(path.dirname(file), { recursive: true, force: true }))) }
33
+ }
34
+ }
35
+ async function run() {
36
+ const f = fixture()
37
+ try {
38
+ const a = await f.sessions.start('a', 1001)
39
+ assert.equal(a.state, 'recording')
40
+ assert.equal(a.capture_completed_at_ms, null)
41
+ await assert.rejects(() => f.sessions.start('a'), /TRACE_BUSY/)
42
+ const b = await f.sessions.start('b')
43
+ const end = await f.sessions.link(a.trace_id, { platform: 'android', id: 'a' }, 'journey-a')
44
+ await assert.rejects(() => f.sessions.link(a.trace_id, { platform: 'android', id: 'b' }, 'wrong'), /TRACE_NOT_FOUND/)
45
+ await assert.rejects(() => f.sessions.link(a.trace_id, { platform: 'ios', id: 'a' }, 'wrong'), /TRACE_NOT_FOUND/)
46
+ const [stop, again] = await Promise.all([f.sessions.control(a.trace_id, 'stop'), f.sessions.control(a.trace_id, 'stop')])
47
+ assert.equal(stop.state, 'completed'); assert.deepEqual(stop, again)
48
+ f.files.push(stop.artifact!.path)
49
+ assert.equal((await end()).coverage, 'partial') // another call stopped capture during the linked journey
50
+ assert.deepEqual(stop.correlation_ids, ['journey-a'])
51
+ assert.equal((await fs.stat(stop.artifact!.path)).mode & 0o777, 0o600)
52
+ assert.equal((await f.sessions.control(a.trace_id, 'cancel')).artifact?.path, stop.artifact?.path)
53
+ assert.equal(f.counts.pulls, 1)
54
+ assert.equal((await f.sessions.control(b.trace_id, 'cancel')).state, 'cancelled')
55
+ assert.equal(f.counts.pulls, 1)
56
+ const expired = await f.sessions.start('c', 1000)
57
+ f.tick(1001)
58
+ const duration = await f.sessions.control(expired.trace_id, 'status')
59
+ assert.equal(duration.completion_reason, 'duration_limit'); f.files.push(duration.artifact!.path)
60
+ f.tick(600001)
61
+ await assert.rejects(() => f.sessions.control(a.trace_id, 'status'), /TRACE_NOT_FOUND/)
62
+ await assert.rejects(() => f.sessions.control('previous-server', 'stop'), /TRACE_NOT_FOUND/)
63
+ } finally { await f.cleanup() }
64
+
65
+ for (const failure of ['start', 'stop', 'pull', 'remove', 'find', 'empty', 'oversized'] as const) {
66
+ const f = fixture()
67
+ try {
68
+ if (failure === 'start') f.flags({ start: 'ACTION_TIMEOUT' })
69
+ const a = await f.sessions.start('a')
70
+ if (failure === 'start') {
71
+ assert.equal(a.reason, 'ACTION_TIMEOUT'); assert.equal(a.state, 'failed'); assert.equal(a.cleanup.status, 'complete')
72
+ assert.equal(f.processes.size, 0); continue
73
+ }
74
+ f.flags(failure === 'empty' || failure === 'oversized' ? { [failure]: true } : { [failure]: 'ACTION_TIMEOUT' })
75
+ const stop = await f.sessions.control(a.trace_id, 'stop')
76
+ assert.equal(stop.state, 'failed', failure); assert.equal(stop.cleanup.status, 'failed', failure)
77
+ if (stop.artifact) f.files.push(stop.artifact.path)
78
+ if (failure === 'pull' || failure === 'empty' || failure === 'oversized') assert.equal(stop.artifact, null)
79
+ f.flags({})
80
+ const retry = await f.sessions.control(a.trace_id, 'stop')
81
+ assert.equal(retry.state, 'completed', failure); assert.equal(retry.cleanup.status, 'complete')
82
+ if (!f.files.includes(retry.artifact!.path)) f.files.push(retry.artifact!.path)
83
+ } finally { await f.cleanup() }
84
+ }
85
+ const reuse = fixture()
86
+ try {
87
+ const a = await reuse.sessions.start('a')
88
+ const [remote, process] = [...reuse.processes.entries()][0]
89
+ reuse.processes.set(remote, { ...process, startTime: '999999' })
90
+ const result = await reuse.sessions.control(a.trace_id, 'stop')
91
+ assert.equal(result.reason, 'TRACE_STOP_FAILED'); assert.equal(reuse.counts.stops, 0)
92
+ reuse.processes.delete(remote)
93
+ assert.equal((await reuse.sessions.control(a.trace_id, 'cancel')).state, 'cancelled')
94
+ } finally { await reuse.cleanup() }
95
+ const cap = fixture()
96
+ try {
97
+ cap.flags({ supported: false })
98
+ assert.equal((await cap.sessions.start('a')).reason, 'TRACE_UNSUPPORTED'); assert.equal(cap.counts.starts, 0)
99
+ cap.flags({})
100
+ await Promise.all(['a', 'b', 'c', 'd'].map(d => cap.sessions.start(d)))
101
+ await assert.rejects(() => cap.sessions.start('e'), /TRACE_BUSY/)
102
+ await cap.sessions.shutdown(); assert.equal(cap.processes.size, 0)
103
+ await assert.rejects(() => cap.sessions.start('e'), /TRACE_BUSY/)
104
+ } finally { await cap.cleanup() }
105
+ const timer = fixture()
106
+ try {
107
+ const a = await timer.sessions.start('timer', 1000)
108
+ timer.tick(1000)
109
+ for (const callback of timer.pending.values()) callback()
110
+ const ended = await timer.sessions.control(a.trace_id, 'status')
111
+ assert.equal(ended.state, 'completed'); assert.equal(ended.completion_reason, 'duration_limit')
112
+ timer.files.push(ended.artifact!.path)
113
+ } finally { await timer.cleanup() }
114
+ const stuck = fixture()
115
+ try {
116
+ const a = await stuck.sessions.start('stuck')
117
+ stuck.flags({ stuck: true })
118
+ const failed = await stuck.sessions.control(a.trace_id, 'stop')
119
+ assert.equal(failed.reason, 'ACTION_TIMEOUT'); assert.equal(failed.artifact, null)
120
+ assert.equal(stuck.counts.pulls, 0)
121
+ stuck.flags({})
122
+ assert.equal((await stuck.sessions.control(a.trace_id, 'cancel')).cleanup.status, 'complete')
123
+ } finally { await stuck.cleanup() }
124
+ const natural = fixture()
125
+ try {
126
+ const a = await natural.sessions.start('natural')
127
+ natural.native.stop = async (_device, remote) => { natural.processes.delete(remote); throw new Error('PROCESS_ALREADY_EXITED') }
128
+ assert.equal((await natural.sessions.control(a.trace_id, 'cancel')).state, 'cancelled')
129
+ } finally { await natural.cleanup() }
130
+ const interrupted = fixture()
131
+ try {
132
+ const a = await interrupted.sessions.start('journey')
133
+ const finish = await interrupted.sessions.link(a.trace_id, { platform: 'android', id: 'journey' }, 'j')
134
+ interrupted.flags({ find: 'DEVICE_DISCONNECTED' })
135
+ assert.equal((await finish()).coverage, 'partial')
136
+ interrupted.flags({})
137
+ } finally { await interrupted.cleanup() }
138
+ const racing = fixture()
139
+ let release!: () => void
140
+ const gate = new Promise<void>(resolve => { release = resolve })
141
+ const originalCapabilities = racing.native.capabilities
142
+ racing.native.capabilities = async device => { await gate; return originalCapabilities(device) }
143
+ const starting = racing.sessions.start('race')
144
+ const closing = racing.sessions.shutdown()
145
+ release()
146
+ assert.equal((await starting).reason, 'TRACE_BUSY')
147
+ assert.deepEqual(await closing, [])
148
+ assert.equal(racing.counts.starts, 0)
149
+ const retention = fixture()
150
+ try {
151
+ const first = await retention.sessions.start('retention')
152
+ await retention.sessions.control(first.trace_id, 'cancel')
153
+ for (let n = 0; n < 100; n++) {
154
+ const next = await retention.sessions.start('retention')
155
+ await retention.sessions.control(next.trace_id, 'cancel')
156
+ }
157
+ await assert.rejects(() => retention.sessions.control(first.trace_id, 'status'), /TRACE_NOT_FOUND/)
158
+ } finally { await retention.cleanup() }
159
+ // Wire bytes decode independently to protect actual proto field numbers/limits.
160
+ const config = traceConfig(1001, ['sched', 'input'])
161
+ assert.ok(config.includes(Buffer.from('linux.ftrace')))
162
+ assert.ok(config.includes(Buffer.from([0x18, 0xe9, 0x07]))) // TraceConfig.duration_ms = 3
163
+ assert.ok(config.includes(Buffer.from([0x50, 0x80, 0x80, 0x80, 0x10]))) // max_file_size_bytes = 10
164
+ console.log('system trace session lifecycle: PASS')
165
+ }
166
+ run().catch(error => { console.error(error); process.exitCode = 1 })
@@ -0,0 +1,60 @@
1
+ import assert from 'node:assert/strict'
2
+ import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
3
+ import { tmpdir } from 'node:os'
4
+ import path from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
6
+ import { architectureGraph } from '../../../scripts/architecture-graph.mjs'
7
+ import { checkArchitecture } from '../../../scripts/check-architecture.mjs'
8
+
9
+ const root = await mkdtemp(path.join(tmpdir(), 'mcp-architecture-'))
10
+ try {
11
+ const files: Record<string, string> = {
12
+ 'package.json': JSON.stringify({ type: 'module' }),
13
+ 'tsconfig.json': JSON.stringify({ compilerOptions: {
14
+ module: 'NodeNext', moduleResolution: 'NodeNext', target: 'ES2022',
15
+ paths: { '@server/*': ['./src/server/*'] }
16
+ }, include: ['src/**/*.ts'] }),
17
+ 'src/server/entry.ts': 'export const value = 1',
18
+ 'src/contracts/bad.ts': "export { value } from '@server/entry.js'",
19
+ 'src/interact/entry.ts': "export const load = () => import('../server/entry.js')",
20
+ 'src/interact/adjustment/control-values.ts': "import { spawn } from 'node:child_process'; export const native = spawn",
21
+ 'src/cycle/a.ts': "export { b } from './b.js'; export const a = 1",
22
+ 'src/cycle/b.ts': "export const b = () => import('./a.js')",
23
+ 'src/type-cycle/a.ts': "import type { B } from './b.js'; export interface A { b: B }",
24
+ 'src/type-cycle/b.ts': "import { type A } from './a.js'; export interface B { a: A }",
25
+ 'src/import-equals.ts': "import entry = require('./server/entry.js'); export { entry }",
26
+ 'src/require.ts': "export const load = () => require('./server/entry.js')"
27
+ }
28
+ for (const [file, code] of Object.entries(files)) {
29
+ const target = path.join(root, file)
30
+ await mkdir(path.dirname(target), { recursive: true })
31
+ await writeFile(target, code)
32
+ }
33
+ const config = path.join(root, 'tsconfig.json')
34
+ const graph = await architectureGraph(config)
35
+ const edge = (source: string, target: string) => graph.find(item => item.source === source && item.target === target)
36
+ assert(edge('src/contracts/bad.ts', 'src/server/entry.ts'), 'Resolve aliased re-exports to .ts sources')
37
+ assert(edge('src/interact/entry.ts', 'src/server/entry.ts'), 'Resolve literal dynamic .js imports')
38
+ assert(edge('src/require.ts', 'src/server/entry.ts'), 'Resolve literal require calls')
39
+ assert(edge('src/interact/adjustment/control-values.ts', 'node:child_process'), 'Include Node builtins')
40
+ assert.deepEqual(edge('src/type-cycle/b.ts', 'src/type-cycle/a.ts')?.importKinds, ['type'])
41
+ assert(edge('src/import-equals.ts', 'src/server/entry.ts'), 'Resolve import-equals require syntax')
42
+ const rejected = spawnSync(process.execPath, ['scripts/check-architecture.mjs', config], { encoding: 'utf8' })
43
+ assert.equal(rejected.status, 1, rejected.stderr)
44
+ assert(rejected.stderr.includes('Production runtime dependencies must be cycle-free'))
45
+ const results = checkArchitecture(graph)
46
+ assert(results.every(result => result.violations.length > 0), 'Every rule must reject its deliberately broken fixture')
47
+ const cycles = JSON.stringify(results.at(-1)?.violations)
48
+ assert(cycles.includes('src/cycle/a.ts'), 'Catch a cycle through a re-export and dynamic import')
49
+ assert(!cycles.includes('src/type-cycle'), 'Type-only cycles are not runtime cycles')
50
+ assert(checkArchitecture([]).every(result => result.violations.length > 0), 'An empty graph must fail closed')
51
+
52
+ await writeFile(path.join(root, 'src/nonliteral.ts'), 'export const load = (name: string) => import(name)')
53
+ await assert.rejects(architectureGraph(config), /Nonliteral dependency/)
54
+ await rm(path.join(root, 'src/nonliteral.ts'))
55
+ await writeFile(path.join(root, 'src/unresolved.ts'), "import './missing.js'")
56
+ await assert.rejects(architectureGraph(config), /Unresolved dependency/)
57
+ console.log('PASS architecture negative controls: NodeNext, aliases, re-exports, dynamic/require imports, pure boundaries, runtime cycles, type-only cycles, empty and unresolved graphs')
58
+ } finally {
59
+ await rm(root, { recursive: true, force: true })
60
+ }
@@ -0,0 +1,58 @@
1
+ import assert from 'node:assert/strict'
2
+ import { measure, compare } from '../../../scripts/code-quality.mjs'
3
+
4
+ const file = 'src/interact/quality-fixture.ts'
5
+ const inspect = (code: string) => measure(file, code)
6
+ const has = (findings: Awaited<ReturnType<typeof inspect>>, rule: string) => findings.some(f => f.rule === rule)
7
+ const baseline = (findings: Awaited<ReturnType<typeof inspect>>) => findings.map(({ line: _line, ...entry }) => ({ ...entry, reason: 'Test legacy exception' }))
8
+
9
+ async function main() {
10
+ assert.deepEqual(await inspect('export const twice = (value: number) => value * 2'), [])
11
+ const hugeClass = await inspect(`export class Huge {\n${Array.from({ length: 301 }, (_, i) => `field${i} = ${i}`).join('\n')}\n}`)
12
+ assert(has(hugeClass, 'class-lines'))
13
+ const hugeFunction = await inspect(`export function work() {\n${Array.from({ length: 81 }, (_, i) => `console.log(${i})`).join('\n')}\n}`)
14
+ assert(has(hugeFunction, 'max-lines-per-function'))
15
+ assert(has(await inspect(`export const work = () => {\n${'console.log(1)\n'.repeat(81)}}`), 'max-lines-per-function'))
16
+ const hugeFile = await inspect(Array.from({ length: 501 }, (_, i) => `export const value${i} = ${i}`).join('\n'))
17
+ assert(has(hugeFile, 'max-lines'))
18
+ assert(!has(await inspect(`// A comment\n${'\n// comment\n'.repeat(600)}export const x = 1`), 'max-lines'))
19
+ const complex = await inspect(`export function decide(value: number) { ${'if(value) console.log(value);'.repeat(21)} }`)
20
+ assert(has(complex, 'complexity'))
21
+ assert(has(await inspect('export function nested(x: boolean) { if(x){if(x){if(x){if(x){if(x){console.log(x)}}}}} }'), 'max-depth'))
22
+ assert(has(await inspect("import { value } from '../server/tool-handlers.js'"), 'import-boundary'))
23
+ assert(has(await inspect("export { value } from '../server/common.js'"), 'import-boundary'))
24
+ assert(has(await inspect("type Args = import('../server/common.js').ToolCallArgs"), 'import-boundary'))
25
+ assert(has(await inspect("const transport = require('../server/common.js')"), 'import-boundary'))
26
+ assert(has(await inspect('/* eslint complexity: \"off\" */\nexport const value = 1'), 'inline-suppression'))
27
+ assert(!has(await inspect("import type { Value } from '../contracts/value.js'"), 'import-boundary'))
28
+ assert(has(await inspect('// eslint-disable\nexport const x = 1'), 'inline-suppression'))
29
+ assert(has(await inspect('// eslint-disable-next-line complexity\nexport function f() {}'), 'inline-suppression'))
30
+ const any = await inspect('export function legacy(value: any) { return value }')
31
+ assert(has(any, 'explicit-any'))
32
+ assert.deepEqual(compare(any, baseline(any)), [])
33
+ assert(compare(any, []).some(e => e.includes('explicit-any')))
34
+ const grown = await inspect('export function legacy(value: any): any { return value }')
35
+ assert(compare(grown, baseline(any)).some(e => e.includes('explicit-any')))
36
+ const renamed = await inspect('export function replacement(value: any) { return value }')
37
+ assert(compare(renamed, baseline(any)).length >= 2, 'renames do not inherit debt')
38
+ const moved = any.map(f => ({ ...f, file: 'src/interact/moved.ts' }))
39
+ assert(compare(moved, baseline(any)).length >= 2, 'moves do not inherit debt')
40
+ assert(compare([], baseline(any)).some(e => e.includes('obsolete')))
41
+ assert(compare(any, baseline(grown)).some(e => e.includes('Lower baseline')))
42
+ assert(compare(any, [...baseline(any), ...baseline(any)]).some(e => e.includes('Duplicate')))
43
+ assert(compare(any, [{ ...baseline(any)[0], reason: '' }]).some(e => e.includes('Invalid')))
44
+ const collision = await inspect('function same(x: number) {return x} function same(x: number) {return x}')
45
+ assert(has(collision, 'duplicate-symbol'))
46
+ assert(compare(collision, baseline(collision)).some(e => e.includes('duplicate-symbol')), 'identity collisions cannot be baseline exceptions')
47
+ const suppressed = await inspect('// eslint-disable max-lines-per-function\n'+`function huge(){\n${'console.log(1)\n'.repeat(81)}}`)
48
+ assert(has(suppressed, 'max-lines-per-function'), 'inline directives cannot hide metrics')
49
+ const typeDebt = await inspect('type First = any; type Second = number')
50
+ const shiftedTypeDebt = await inspect('type First = number; type Second = any')
51
+ assert(compare(shiftedTypeDebt, baseline(typeDebt)).length >= 2, 'unrelated type declarations cannot offset each other')
52
+ const java = await measure('src/debugger/java/Fixture.java', 'int field;\n'.repeat(501))
53
+ assert(has(java, 'max-lines'))
54
+ assert.deepEqual(await measure('src/debugger/java/Fixture.java', '// comment\n'.repeat(501)), [])
55
+ assert(!has(await inspect('class Accessor { get value() { return 1 } set value(value: number) { console.log(value) } }'), 'duplicate-symbol'))
56
+ console.log('PASS structural metrics, debt ratchet, boundaries and suppression protection')
57
+ }
58
+ main().catch(error => { console.error(error); process.exitCode = 1 })
@@ -0,0 +1,24 @@
1
+ import assert from 'node:assert/strict'
2
+ import { readdir, readFile } from 'node:fs/promises'
3
+ import { measure } from '../../../scripts/code-quality.mjs'
4
+
5
+ // Refactored interaction services must meet defaults even if someone adds a legacy allowance.
6
+ const legacy = new Set(['android.ts', 'ios.ts', 'conditional-action.ts', 'classify.ts'])
7
+ async function sourceFiles(directory = ''): Promise<string[]> {
8
+ const entries = await readdir(`src/interact/${directory}`, { withFileTypes: true })
9
+ const paths: string[] = []
10
+ for (const entry of entries) {
11
+ const relative = directory ? `${directory}/${entry.name}` : entry.name
12
+ if (entry.isDirectory()) paths.push(...await sourceFiles(relative))
13
+ else if (relative.endsWith('.ts') && !legacy.has(relative)) paths.push(relative)
14
+ }
15
+ return paths
16
+ }
17
+ const files = await sourceFiles()
18
+ const baseline = JSON.parse(await readFile('scripts/code-quality-baseline.json', 'utf8')) as Array<{ file: string }>
19
+ for (const file of files) {
20
+ const path = `src/interact/${file}`
21
+ assert.deepEqual(await measure(path, await readFile(path, 'utf8')), [], `${path} must meet default structural and typing limits`)
22
+ assert(!baseline.some(entry => entry.file === path), `${path} cannot inherit legacy exceptions`)
23
+ }
24
+ console.log(`PASS default quality limits and no legacy exceptions across ${files.length} interaction modules`)
@@ -4,7 +4,7 @@ import { tmpdir } from 'node:os'
4
4
  import path from 'node:path'
5
5
  import { handleToolCall, toolDefinitions } from '../../../src/server-core.js'
6
6
  import { ToolsObserve } from '../../../src/observe/index.js'
7
- import { ToolsNetwork } from '../../../src/network/index.js'
7
+ import { ToolsNetwork } from '../../../src/observe/network/index.js'
8
8
  import { deriveSnapshotMetadata, getSnapshotSessionId, resetSnapshotMetadataForTests } from '../../../src/observe/snapshot-metadata.js'
9
9
  import { _setDeviceListersForTests, _resetDeviceListersForTests } from '../../../src/utils/resolve-device.js'
10
10
  import { runWithBudget } from '../../../src/utils/operation-budget.js'
@@ -1,8 +1,8 @@
1
1
  import assert from 'node:assert/strict'
2
2
  import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv'
3
3
  import { toolDefinitions } from '../../../src/server/tool-definitions.js'
4
- import { conditionalTools, validateExpectedState } from '../../../src/server/condition-schema.js'
5
- import { journeySchema } from '../../../src/server/journey-schema.js'
4
+ import { conditionalTools, validateExpectedState } from '../../../src/server/schemas/condition-schema.js'
5
+ import { journeySchema } from '../../../src/server/schemas/journey-schema.js'
6
6
 
7
7
  const provider = new AjvJsonSchemaValidator()
8
8
  const expectedState = { snapshotSessionId: 'session-fixture', snapshotRevision: 1 }
@@ -0,0 +1,63 @@
1
+ import assert from 'node:assert/strict'
2
+ import { debuggerResponse } from '../../../src/server/handlers/debugger-response.js'
3
+ import { handleDebugger } from '../../../src/server/handlers/debugger-handlers.js'
4
+ import { debuggerContracts, debuggerTools } from '../../../src/server/schemas/debugger-schema.js'
5
+ import { debuggerSessions } from '../../../src/debugger/sessions.js'
6
+ import { DebuggerError } from '../../../src/debugger/worker.js'
7
+ import { diagnostic } from '../../../src/debugger/diagnostics.js'
8
+
9
+ const baseline = ['capabilities', 'attach', 'state']
10
+ const payload = {
11
+ success: false, debug_session_id: 'session', device: { id: 'device' }, target: { pid: 42 }, capabilities: { locals: true },
12
+ state: 'paused', captured_at_ms: 123, reason: null, stop: { stop_id: 'stop', thread_id: 'thread' },
13
+ limitations: ['SHALLOW_LOCALS'], cleanup: { status: 'failed', remaining_resources: ['forward:tcp:123'] },
14
+ error: { code: 'DEBUGGER_CLEANUP_FAILED', retryable: false, diagnostic: diagnostic('cleanup', 'operation_failed') },
15
+ execution_state: 'unknown', result: { frames: [{ frame_id: 'frame' }], truncated: true, availability: 'unknown' },
16
+ breakpoints: [], events: [], next_cursor: 4, overflow: true, timed_out: false, future_field: { preserve: true }
17
+ }
18
+ const expected = structuredClone(payload) as Record<string, unknown>
19
+ for (const key of ['device', 'target', 'capabilities']) delete expected[key]
20
+ const before = structuredClone(payload)
21
+ for (const tool of debuggerTools) {
22
+ const contract = debuggerContracts[tool.name]
23
+ assert.deepEqual(tool.inputSchema.properties.responseMode, { type: 'string', enum: ['compact', 'debug'], default: 'debug' })
24
+ const shape = contract.schema.shape
25
+ assert.equal(shape.responseMode.parse(undefined), 'debug')
26
+ for (const mode of ['compact', 'debug']) assert.equal(shape.responseMode.parse(mode), mode)
27
+ assert.equal(shape.responseMode.safeParse('full').success, false)
28
+ assert.deepEqual(debuggerResponse(contract.command, 'debug', payload), payload)
29
+ assert.deepEqual(debuggerResponse(contract.command, 'compact', payload), baseline.includes(contract.command) ? payload : expected)
30
+ const success = { ...payload, success: true, error: undefined, cleanup: { status: 'pending', remaining_resources: ['jdi_worker'] } }
31
+ assert.deepEqual(debuggerResponse(contract.command, 'compact', success), baseline.includes(contract.command) ? success : { ...expected, success: true, error: undefined, cleanup: success.cleanup })
32
+ const noSession = { ...payload, debug_session_id: null }
33
+ assert.deepEqual(debuggerResponse(contract.command, 'compact', noSession), noSession)
34
+ }
35
+ assert.deepEqual(payload, before, 'projection must not mutate nested or shared objects')
36
+
37
+ // This self-running test has its own process; restore the singleton seam in finally.
38
+ const original = debuggerSessions.command
39
+ try {
40
+ debuggerSessions.command = async (_id, _command, args) => {
41
+ assert.ok(!Object.hasOwn(args!, 'responseMode'), 'presentation input must not reach worker')
42
+ return { ...payload, success: true } as unknown as Awaited<ReturnType<typeof original>>
43
+ }
44
+ for (const mode of [undefined, 'debug', 'compact']) {
45
+ const response = await handleDebugger('get_debugger_locals', { debugSessionId: 'session', stopId: 'stop', frameId: 'frame', ...(mode ? { responseMode: mode } : {}) })
46
+ assert.equal(response.isError, undefined)
47
+ assert.deepEqual(JSON.parse(response.content[0].text), { ...(mode === 'compact' ? expected : payload), success: true })
48
+ }
49
+ for (const code of ['DEBUGGER_STALE_REFERENCE', 'DEBUGGER_NOT_FOUND', 'DEBUGGER_WORKER_FAILED', 'DEBUGGER_TIMEOUT', 'DEBUGGER_CANCELLED']) {
50
+ debuggerSessions.command = async () => { throw new DebuggerError(code, diagnostic('command', 'unknown')) }
51
+ for (const responseMode of ['compact', 'debug']) {
52
+ const response = await handleDebugger('resume_debugger', { debugSessionId: 'absent', stopId: 'old', responseMode })
53
+ const value = JSON.parse(response.content[0].text)
54
+ assert.equal(response.isError, true)
55
+ assert.equal(value.error.code, code)
56
+ assert.equal(Object.hasOwn(value, 'capabilities'), responseMode === 'debug')
57
+ assert.equal(value.debug_session_id, 'absent')
58
+ }
59
+ }
60
+ const invalid = await handleDebugger('detach_debugger', { debugSessionId: 'session', responseMode: 'full' })
61
+ assert.equal(JSON.parse(invalid.content[0].text).error.code, 'INVALID_ARGUMENT')
62
+ } finally { debuggerSessions.command = original }
63
+ console.log('PASS debugger response defaults, exact retention, failures, immutable projection and argument isolation')
@@ -0,0 +1,64 @@
1
+ import assert from 'node:assert/strict'
2
+ import { handleToolCall, toolDefinitions } from '../../../src/server-core.js'
3
+ import { systemTraces } from '../../../src/observe/system-trace.js'
4
+ import { _setDeviceListersForTests, _resetDeviceListersForTests } from '../../../src/utils/resolve-device.js'
5
+ import { AndroidManage } from '../../../src/manage/index.js'
6
+ import { withEvidence, bindSystemTrace, bindEvidenceDevice } from '../../../src/observe/evidence.js'
7
+
8
+ const parse = (r: any) => JSON.parse(r.content[0].text)
9
+ async function run() {
10
+ const originalLink = systemTraces.link, originalStart = systemTraces.start, originalControl = systemTraces.control
11
+ const originalApp = AndroidManage.prototype.startApp
12
+ const id = '62a51510-c303-4a1b-af0e-f97cc976d740'
13
+ let dispatched = 0, starts = 0
14
+ _setDeviceListersForTests({ listAndroidDevices: async () => [{ id: 'trace-device', platform: 'android', osVersion: '16', simulator: true, model: 'fixture' }] })
15
+ try {
16
+ for (const name of ['get_system_trace_capabilities', 'start_system_trace', 'get_system_trace_status', 'stop_system_trace', 'cancel_system_trace']) {
17
+ const tool = toolDefinitions.find(t => t.name === name)
18
+ assert.ok(tool); assert.equal(tool.inputSchema.additionalProperties, false)
19
+ }
20
+ assert.equal(toolDefinitions.find(t => t.name === 'run_journey').inputSchema.properties.systemTraceId.format, 'uuid')
21
+ for (const args of [ {}, { platform: 'android', extra: true }, { platform: 'android', expectedState: {} }, { platform: 'android', durationMs: 0 }, { platform: 'android', durationMs: 1000.5 }, { platform: 'android', durationMs: 120001 } ]) {
22
+ const r = await handleToolCall('start_system_trace', args)
23
+ assert.equal(r.isError, true); assert.equal(parse(r).error.code, 'INVALID_ARGUMENT')
24
+ }
25
+ systemTraces.start = async () => { starts++; throw new Error('must not run') }
26
+ const ios = await handleToolCall('start_system_trace', { platform: 'ios' })
27
+ assert.equal(parse(ios).error.code, 'UNSUPPORTED_PLATFORM'); assert.equal(starts, 0)
28
+ assert.equal(parse(await handleToolCall('get_system_trace_capabilities', { platform: 'ios' })).supported, false)
29
+ for (const name of ['stop_system_trace', 'cancel_system_trace', 'get_system_trace_status']) {
30
+ assert.equal(parse(await handleToolCall(name, { traceId: id, deviceId: 'wrong' })).error.code, 'INVALID_ARGUMENT')
31
+ assert.equal(parse(await handleToolCall(name, { traceId: id })).error.code, 'TRACE_NOT_FOUND')
32
+ }
33
+ AndroidManage.prototype.startApp = async () => { dispatched++; return { appStarted: true } as any }
34
+ const journey = { platform: 'android', deviceId: 'trace-device', systemTraceId: id, steps: [{ id: 'start', type: 'start_app', appId: 'com.android.settings', verificationMode: 'none' }] }
35
+ const rejected = await handleToolCall('run_journey', journey)
36
+ assert.equal(rejected.isError, true); assert.equal(dispatched, 0)
37
+ let correlation = ''
38
+ systemTraces.link = async (trace, device, cid) => {
39
+ assert.equal(trace, id); assert.equal(device.id, 'trace-device'); correlation = cid
40
+ return async () => ({ trace_id: trace, coverage: 'covered', clock_alignment: 'unknown' })
41
+ }
42
+ const result = parse(await handleToolCall('run_journey', journey))
43
+ assert.equal(result.success, true, JSON.stringify(result)); assert.equal(dispatched, 1)
44
+ assert.equal(result.evidence.trace_context.trace_id, id)
45
+ assert.equal(result.evidence.trace_context.supported, true)
46
+ assert.equal(result.evidence.correlation_id, correlation)
47
+ assert.equal(result.steps[0].result.evidence.trace_context.trace_id, id)
48
+ assert.equal(result.steps[0].result.evidence.correlation_id, correlation)
49
+ assert.equal(result.system_trace.coverage, 'covered')
50
+ const plain = parse(await handleToolCall('run_journey', { ...journey, systemTraceId: undefined }))
51
+ assert.deepEqual(plain.evidence.trace_context, { supported: false, trace_id: null, markers: [] })
52
+ const [linked, independent] = await Promise.all([
53
+ withEvidence({ journey: true }, async () => { bindSystemTrace(id); bindEvidenceDevice({ platform: 'android', id: 'trace-device' }); return withEvidence({ action: true }, async () => {}) }),
54
+ withEvidence({ journey: true }, async () => {})
55
+ ])
56
+ assert.equal(linked.result.evidence.trace_context.trace_id, id)
57
+ assert.equal(independent.evidence.trace_context.trace_id, null)
58
+ console.log('system trace tool contracts and journey evidence: PASS')
59
+ } finally {
60
+ systemTraces.link = originalLink; systemTraces.start = originalStart; systemTraces.control = originalControl
61
+ AndroidManage.prototype.startApp = originalApp; _resetDeviceListersForTests()
62
+ }
63
+ }
64
+ run().catch(error => { console.error(error); process.exitCode = 1 })
@@ -0,0 +1,8 @@
1
+ import assert from 'node:assert/strict'
2
+ import { readFile } from 'node:fs/promises'
3
+ import { toolDefinitions } from '../../../src/server/tool-definitions.js'
4
+
5
+ // Captured before structural refactoring: changes require an intentional contract review.
6
+ const baseline = JSON.parse(await readFile(new URL('../../fixtures/contracts/tool-definitions.json', import.meta.url), 'utf8'))
7
+ assert.deepEqual(toolDefinitions, baseline, 'Public tool order, schemas, defaults and descriptions must remain compatible')
8
+ console.log('PASS public tool-definition compatibility baseline')