mobile-debug-mcp 0.35.0 → 0.37.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 (247) hide show
  1. package/.cursor-plugin/marketplace.json +1 -1
  2. package/.github/workflows/ci.yml +1 -1
  3. package/README.md +96 -24
  4. package/agent-plugin/.antigravity-plugin/README.md +48 -0
  5. package/agent-plugin/.antigravity-plugin/plugin.json +18 -0
  6. package/agent-plugin/.claude-plugin/plugin.json +2 -2
  7. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  8. package/agent-plugin/.cursor-plugin/plugin.json +2 -2
  9. package/agent-plugin/.mcp.json +1 -1
  10. package/agent-plugin/README.md +37 -8
  11. package/agent-plugin/cursor/.cursor-plugin/plugin.json +23 -0
  12. package/agent-plugin/cursor/LICENSE +201 -0
  13. package/agent-plugin/cursor/README.md +9 -0
  14. package/agent-plugin/cursor/skills/mobile-debug-mcp/SKILL.md +261 -0
  15. package/agent-plugin/mcp.json +1 -1
  16. package/agent-plugin/mcp_config.json +13 -0
  17. package/agent-plugin/plugin.json +1 -1
  18. package/agent-plugin/skills/mobile-debug-mcp/SKILL.md +46 -18
  19. package/dist/contracts/device-readiness.js +1 -0
  20. package/dist/debugger/guard.js +4 -4
  21. package/dist/debugger/ios-worker.js +62 -0
  22. package/dist/debugger/ios.js +57 -0
  23. package/dist/debugger/sessions.js +33 -16
  24. package/dist/debugger/worker.js +6 -4
  25. package/dist/interact/index.js +12 -2
  26. package/dist/interact/waits/ui-change-wait.js +9 -2
  27. package/dist/interact/waits/ui-wait.js +7 -3
  28. package/dist/manage/index.js +3 -0
  29. package/dist/manage/ios.js +2 -0
  30. package/dist/observe/device-readiness.js +116 -0
  31. package/dist/observe/evidence.js +31 -0
  32. package/dist/observe/ios-trace/artifact.js +63 -0
  33. package/dist/observe/ios-trace/capabilities.js +86 -0
  34. package/dist/observe/ios-trace/native.js +93 -0
  35. package/dist/observe/ios-trace/notification.js +35 -0
  36. package/dist/observe/ios-trace/process.js +98 -0
  37. package/dist/observe/ios-trace/sessions.js +246 -0
  38. package/dist/observe/readiness-service.js +22 -10
  39. package/dist/observe/readiness.js +34 -4
  40. package/dist/observe/screenshot-encoding.js +4 -0
  41. package/dist/observe/snapshot-collection.js +7 -2
  42. package/dist/observe/system-trace-error.js +7 -0
  43. package/dist/observe/system-trace-router.js +81 -0
  44. package/dist/observe/system-trace.js +4 -8
  45. package/dist/server/definitions/manage.js +3 -2
  46. package/dist/server/handlers/action-execution.js +23 -14
  47. package/dist/server/handlers/compact-interaction.js +106 -0
  48. package/dist/server/handlers/compact-state.js +47 -0
  49. package/dist/server/handlers/debugger-handlers.js +27 -8
  50. package/dist/server/handlers/diagnostic.js +12 -4
  51. package/dist/server/handlers/input.js +2 -0
  52. package/dist/server/handlers/interact.js +2 -0
  53. package/dist/server/handlers/interaction-diagnostics.js +62 -0
  54. package/dist/server/handlers/journey.js +18 -20
  55. package/dist/server/handlers/observe.js +2 -1
  56. package/dist/server/handlers/tool-handlers.js +28 -14
  57. package/dist/server/schemas/debugger-schema.js +10 -7
  58. package/dist/server/schemas/interaction-diagnostics.js +9 -0
  59. package/dist/server/schemas/system-trace-schema.js +14 -9
  60. package/dist/server/tool-definitions.js +9 -4
  61. package/dist/server-core.js +5 -2
  62. package/dist/server.js +5 -5
  63. package/dist/system/android.js +19 -15
  64. package/dist/system/index.js +2 -1
  65. package/dist/utils/android/utils.js +7 -16
  66. package/dist/utils/app-installation.js +77 -0
  67. package/dist/utils/benchmark-metrics.js +43 -0
  68. package/dist/utils/exec.js +4 -0
  69. package/dist/utils/ios/utils.js +7 -11
  70. package/dist/utils/resolve-device.js +3 -2
  71. package/dist/utils/timing.js +3 -3
  72. package/docs/CHANGELOG.md +48 -1
  73. package/docs/agent-plugin-installation-verification.md +39 -0
  74. package/docs/benchmarks/README.md +1 -0
  75. package/docs/benchmarks/end-to-end-latency/README.md +98 -0
  76. package/docs/benchmarks/end-to-end-latency/android-snapshot-pair.json +6532 -0
  77. package/docs/benchmarks/end-to-end-latency/android-snapshot-pair.md +30 -0
  78. package/docs/benchmarks/end-to-end-latency/baseline-attempt-1.json +15901 -0
  79. package/docs/benchmarks/end-to-end-latency/baseline-attempt-1.md +14 -0
  80. package/docs/benchmarks/end-to-end-latency/baseline.json +233843 -0
  81. package/docs/benchmarks/end-to-end-latency/baseline.md +21 -0
  82. package/docs/benchmarks/end-to-end-latency/investigation.md +128 -0
  83. package/docs/benchmarks/end-to-end-latency/ios-smoke.json +84 -0
  84. package/docs/benchmarks/end-to-end-latency/ios-smoke.md +9 -0
  85. package/docs/benchmarks/end-to-end-latency/smoke.json +19601 -0
  86. package/docs/benchmarks/end-to-end-latency/smoke.md +21 -0
  87. package/docs/code-structure.md +3 -1
  88. package/docs/specs/031-ios-system-traces/intent.md +38 -0
  89. package/docs/specs/031-ios-system-traces/investigation.md +92 -0
  90. package/docs/specs/031-ios-system-traces/late-run-failure-evidence.json +32 -0
  91. package/docs/specs/031-ios-system-traces/plan.md +166 -0
  92. package/docs/specs/031-ios-system-traces/pr102-monitor-evidence.json +22 -0
  93. package/docs/specs/031-ios-system-traces/simulator-evidence.json +47 -0
  94. package/docs/specs/031-ios-system-traces/spec.md +135 -0
  95. package/docs/specs/032-ios-runtime-debugger/evidence.json +176 -0
  96. package/docs/specs/032-ios-runtime-debugger/intent.md +38 -0
  97. package/docs/specs/032-ios-runtime-debugger/plan.md +95 -0
  98. package/docs/specs/032-ios-runtime-debugger/report.md +205 -0
  99. package/docs/specs/032-ios-runtime-debugger/spec.md +153 -0
  100. package/docs/specs/033-device-readiness-blockers/android-evidence.json +437 -0
  101. package/docs/specs/033-device-readiness-blockers/android-physical-evidence.json +28 -0
  102. package/docs/specs/033-device-readiness-blockers/android-physical-power-evidence.json +29 -0
  103. package/docs/specs/033-device-readiness-blockers/intent.md +41 -0
  104. package/docs/specs/033-device-readiness-blockers/ios-investigation.md +55 -0
  105. package/docs/specs/033-device-readiness-blockers/ios-probe-evidence.json +24 -0
  106. package/docs/specs/033-device-readiness-blockers/ios-tool-evidence.json +73 -0
  107. package/docs/specs/033-device-readiness-blockers/plan.md +249 -0
  108. package/docs/specs/033-device-readiness-blockers/spec.md +134 -0
  109. package/docs/specs/034-app-installation-status/intent.md +41 -0
  110. package/docs/specs/034-app-installation-status/native-evidence.json +654 -0
  111. package/docs/specs/034-app-installation-status/plan.md +73 -0
  112. package/docs/specs/034-app-installation-status/spec.md +114 -0
  113. package/docs/specs/035-compact-interaction-results/baseline.md +45 -0
  114. package/docs/specs/035-compact-interaction-results/intent.md +38 -0
  115. package/docs/specs/035-compact-interaction-results/plan.md +43 -0
  116. package/docs/specs/035-compact-interaction-results/spec.md +118 -0
  117. package/docs/specs/035-compact-interaction-results/verification.md +111 -0
  118. package/docs/specs/036-end-to-end-interaction-latency/android-snapshot-amendment.md +60 -0
  119. package/docs/specs/036-end-to-end-interaction-latency/android-snapshot-plan.md +32 -0
  120. package/docs/specs/036-end-to-end-interaction-latency/intent.md +37 -0
  121. package/docs/specs/036-end-to-end-interaction-latency/plan.md +59 -0
  122. package/docs/specs/036-end-to-end-interaction-latency/spec.md +126 -0
  123. package/docs/specs/036-end-to-end-interaction-latency/verification.md +87 -0
  124. package/docs/tools/TOOLS.md +3 -2
  125. package/docs/tools/compact-interactions.md +56 -0
  126. package/docs/tools/correlated-evidence.md +6 -0
  127. package/docs/tools/debugger.md +45 -5
  128. package/docs/tools/interact.md +17 -8
  129. package/docs/tools/manage.md +10 -2
  130. package/docs/tools/observe.md +42 -2
  131. package/docs/tools/system-traces.md +81 -14
  132. package/docs/tools/system.md +6 -0
  133. package/package.json +2 -1
  134. package/scripts/agent-plugin.mjs +55 -5
  135. package/src/contracts/device-readiness.ts +8 -0
  136. package/src/debugger/android.ts +1 -1
  137. package/src/debugger/guard.ts +5 -5
  138. package/src/debugger/ios-worker.ts +49 -0
  139. package/src/debugger/ios.ts +45 -0
  140. package/src/debugger/lldb/breakpoints.py +51 -0
  141. package/src/debugger/lldb/inspection.py +109 -0
  142. package/src/debugger/lldb/worker.py +203 -0
  143. package/src/debugger/sessions.ts +24 -12
  144. package/src/debugger/worker.ts +7 -4
  145. package/src/interact/index.ts +13 -2
  146. package/src/interact/waits/ui-change-wait.ts +7 -2
  147. package/src/interact/waits/ui-wait.ts +6 -3
  148. package/src/manage/index.ts +2 -0
  149. package/src/manage/ios.ts +2 -0
  150. package/src/observe/device-readiness.ts +96 -0
  151. package/src/observe/evidence.ts +30 -1
  152. package/src/observe/ios-trace/artifact.ts +55 -0
  153. package/src/observe/ios-trace/capabilities.ts +74 -0
  154. package/src/observe/ios-trace/native.ts +72 -0
  155. package/src/observe/ios-trace/notification.ts +30 -0
  156. package/src/observe/ios-trace/process.ts +82 -0
  157. package/src/observe/ios-trace/sessions.ts +206 -0
  158. package/src/observe/readiness-service.ts +22 -10
  159. package/src/observe/readiness.ts +37 -4
  160. package/src/observe/screenshot-encoding.ts +4 -0
  161. package/src/observe/snapshot-collection.ts +7 -2
  162. package/src/observe/system-trace-error.ts +3 -0
  163. package/src/observe/system-trace-router.ts +61 -0
  164. package/src/observe/system-trace.ts +4 -4
  165. package/src/server/common.ts +2 -2
  166. package/src/server/definitions/manage.ts +3 -2
  167. package/src/server/handlers/action-execution.ts +22 -14
  168. package/src/server/handlers/compact-interaction.ts +91 -0
  169. package/src/server/handlers/compact-state.ts +43 -0
  170. package/src/server/handlers/debugger-handlers.ts +28 -9
  171. package/src/server/handlers/diagnostic.ts +11 -4
  172. package/src/server/handlers/input.ts +2 -0
  173. package/src/server/handlers/interact.ts +2 -0
  174. package/src/server/handlers/interaction-diagnostics.ts +50 -0
  175. package/src/server/handlers/journey.ts +15 -17
  176. package/src/server/handlers/observe.ts +2 -1
  177. package/src/server/handlers/payload.ts +2 -1
  178. package/src/server/handlers/tool-handlers.ts +29 -14
  179. package/src/server/schemas/debugger-schema.ts +10 -7
  180. package/src/server/schemas/interaction-diagnostics.ts +10 -0
  181. package/src/server/schemas/system-trace-schema.ts +15 -9
  182. package/src/server/tool-definitions.ts +8 -4
  183. package/src/server-core.ts +5 -2
  184. package/src/server.ts +4 -4
  185. package/src/system/android.ts +18 -11
  186. package/src/system/index.ts +2 -1
  187. package/src/types.ts +2 -0
  188. package/src/utils/android/utils.ts +8 -10
  189. package/src/utils/app-installation.ts +73 -0
  190. package/src/utils/benchmark-metrics.ts +42 -0
  191. package/src/utils/exec.ts +5 -0
  192. package/src/utils/ios/utils.ts +7 -10
  193. package/src/utils/resolve-device.ts +4 -2
  194. package/src/utils/timing.ts +3 -2
  195. package/test/device/automated/debugger/runtime.android.test.ts +5 -0
  196. package/test/device/automated/debugger/runtime.ios.test.ts +227 -0
  197. package/test/device/automated/interact/compact-results.smoke.ts +58 -0
  198. package/test/device/automated/latency/end-to-end-environment.ts +61 -0
  199. package/test/device/automated/latency/end-to-end-report.ts +90 -0
  200. package/test/device/automated/latency/end-to-end-session.ts +98 -0
  201. package/test/device/automated/latency/end-to-end.ts +149 -0
  202. package/test/device/automated/latency/owned-transport.ts +81 -0
  203. package/test/device/automated/latency/snapshot-pair.ts +143 -0
  204. package/test/device/automated/latency-ios/benchmark.ts +126 -0
  205. package/test/device/automated/observe/correlated-evidence.smoke.ts +7 -5
  206. package/test/device/automated/observe/device-readiness-cleanup.ts +14 -0
  207. package/test/device/automated/observe/device-readiness.android.smoke.ts +78 -0
  208. package/test/device/automated/observe/ios-system-trace.smoke.ts +119 -0
  209. package/test/device/automated/observe/system-trace.smoke.ts +2 -2
  210. package/test/fixtures/contracts/tool-definitions.json +243 -47
  211. package/test/fixtures/ios-debugger-app/Main.swift +95 -0
  212. package/test/fixtures/ios-debugger-app/README.md +27 -0
  213. package/test/fixtures/ios-debugger-app/build.ts +21 -0
  214. package/test/fixtures/latency-journey-app/build.ts +3 -3
  215. package/test/helpers/ios-trace-shutdown.ts +29 -0
  216. package/test/unit/debugger/handle-guards.test.ts +89 -0
  217. package/test/unit/debugger/ios-preparation.test.ts +57 -0
  218. package/test/unit/debugger/ios.test.ts +104 -0
  219. package/test/unit/debugger/lldb-worker.py +166 -0
  220. package/test/unit/debugger/lldb-worker.test.ts +10 -0
  221. package/test/unit/interact/ui-change-readiness-interruption.test.ts +37 -0
  222. package/test/unit/observe/capture_debug_snapshot.test.ts +66 -12
  223. package/test/unit/observe/device-readiness-cleanup.test.ts +24 -0
  224. package/test/unit/observe/device-readiness.test.ts +97 -0
  225. package/test/unit/observe/ios-trace-native.test.ts +90 -0
  226. package/test/unit/observe/ios-trace-sessions.test.ts +154 -0
  227. package/test/unit/observe/readiness-retained-blockers.test.ts +40 -0
  228. package/test/unit/observe/readiness.test.ts +1 -1
  229. package/test/unit/observe/system-trace-router.test.ts +49 -0
  230. package/test/unit/quality/agent-plugin-export.test.ts +38 -0
  231. package/test/unit/server/compact-interaction.test.ts +87 -0
  232. package/test/unit/server/compact-journey-evidence.test.ts +40 -0
  233. package/test/unit/server/compact-review-regressions.test.ts +78 -0
  234. package/test/unit/server/conditional_actions.test.ts +7 -3
  235. package/test/unit/server/correlated-evidence.test.ts +18 -6
  236. package/test/unit/server/end-to-end-report.test.ts +46 -0
  237. package/test/unit/server/input-device-readiness.test.ts +55 -0
  238. package/test/unit/server/interaction-diagnostics.test.ts +45 -0
  239. package/test/unit/server/owned-benchmark-transport.test.ts +32 -0
  240. package/test/unit/server/readiness-retained-deadline.test.ts +32 -0
  241. package/test/unit/server/readiness.test.ts +24 -1
  242. package/test/unit/server/run_journey.test.ts +2 -1
  243. package/test/unit/server/selector_tap_target.test.ts +2 -1
  244. package/test/unit/server/system-trace.test.ts +18 -7
  245. package/test/unit/utils/app-installation.test.ts +78 -0
  246. package/test/unit/utils/benchmark-metrics.test.ts +36 -0
  247. package/test/unit/utils/resolve_device.test.ts +11 -0
@@ -1,3 +1,4 @@
1
+ import { withDeviceReadinessProbe } from '../../../src/observe/device-readiness.js'
1
2
  import assert from 'node:assert/strict'
2
3
  import { AjvJsonSchemaValidator } from '@modelcontextprotocol/sdk/validation/ajv'
3
4
  import { handleToolCall, toolDefinitions } from '../../../src/server-core.js'
@@ -10,6 +11,7 @@ async function run() {
10
11
  const originalTree = ToolsObserve.getUITreeHandler
11
12
  const originalNative = AndroidObserve.prototype.getUITree
12
13
  const device = { platform: 'android', id: 'fixture', model: 'Fixture', osVersion: '16', simulator: true }
14
+ let deviceState = 'unknown'
13
15
  let reads = 0
14
16
  let mode = 'stable'
15
17
  let delayed = 0
@@ -43,12 +45,33 @@ async function run() {
43
45
  queueMicrotask(() => { elapsed += Math.max(0, Number(delay) || 0); callback(...args) })
44
46
  return {} as ReturnType<typeof setTimeout>
45
47
  }) as typeof setTimeout
46
- const call = async (name: string, args: any = {}) => JSON.parse((await handleToolCall(name, { platform: 'android', deviceId: 'fixture', ...args })).content[0].text!)
48
+ const call = async (name: string, args: any = {}) => withDeviceReadinessProbe(async (_cmd, nativeArgs) => {
49
+ const power = 'mWakefulness=Awake\nmWakefulnessChanging=false'
50
+ const keyguard = `KeyguardServiceDelegate\n showing=${deviceState === 'locked'}\n mIsShowing=${deviceState === 'locked'}`
51
+ return { exitCode: 0, stdout: deviceState === 'unknown' ? '' : nativeArgs.includes('power') ? power : keyguard, stderr: '' }
52
+ }, async () => JSON.parse((await handleToolCall(name, { platform: 'android', deviceId: 'fixture', ...args })).content[0].text!))
47
53
  try {
48
54
  let result = await call('get_app_readiness', { stabilityMs: 100 })
49
55
  assert.equal(result.readiness.state, 'settled')
50
56
  assert.equal(result.success, true)
51
57
  assert.equal(reads, 2)
58
+ deviceState = 'locked'; reads = 0
59
+ result = await call('get_app_readiness')
60
+ assert.deepEqual(result.device_readiness.blockers, ['DEVICE_LOCKED'])
61
+ assert.equal(result.success, true, 'an observed blocker is a successful observation')
62
+ assert.equal(reads, 0, 'known blocker avoids a slow UI read')
63
+ result = await call('wait_for_app_ready', { timeoutMs: 300, pollIntervalMs: 100 })
64
+ assert.equal(result.success, false)
65
+ assert.equal(result.failure_code, 'READINESS_TIMEOUT')
66
+ assert.equal(result.device_readiness.input_ready, false)
67
+ const lockedPredicate = await call('wait_for_ui', { selector: { text: 'Ready' }, requireReady: true, timeout_ms: 300, poll_interval_ms: 100 })
68
+ assert.equal(lockedPredicate.status, 'timeout')
69
+ assert.equal(lockedPredicate.device_readiness.lock_state, 'locked')
70
+ deviceState = 'unlocked'; reads = 0
71
+ result = await call('wait_for_app_ready', { stabilityMs: 100, timeoutMs: 1500, pollIntervalMs: 100 })
72
+ assert.equal(result.success, true, 'fresh unlocked evidence permits recovery')
73
+ assert.equal(result.device_readiness.input_ready, null, 'stable UI and unlocked flags do not prove input readiness')
74
+ deviceState = 'unknown'
52
75
  mode = 'loading'; reads = 0
53
76
  result = await call('get_app_readiness')
54
77
  assert.equal(result.readiness.state, 'loading')
@@ -42,7 +42,7 @@ async function run() {
42
42
  assert.strictEqual(result.steps[1].status, 'failed')
43
43
  assert.deepStrictEqual(result.steps[2], {
44
44
  id: 'not-run', type: 'wait', status: 'not_run', success: false,
45
- reason: 'PREVIOUS_STEP_FAILED', result: null, timing: { total_ms: 0 }
45
+ reason: 'PREVIOUS_STEP_FAILED', result: null
46
46
  })
47
47
 
48
48
  let postActionReads = 0
@@ -62,6 +62,7 @@ async function run() {
62
62
  })
63
63
  const light = JSON.parse(lightResponse.content[0].text)
64
64
  assert.strictEqual(light.success, true)
65
+ assert.ok(light.steps[0].result.device_readiness, 'journey action retains device evidence in compact mode')
65
66
  assert.deepStrictEqual(Object.keys(light.timing), ['total_ms'])
66
67
  assert.deepStrictEqual(light.steps[0].result.verification, { mode: 'light', status: 'observed' })
67
68
  assert.strictEqual(light.steps[0].result.verification_diagnostic, undefined)
@@ -43,7 +43,8 @@ async function run() {
43
43
  error = 'Failed to get UI tree: ACTION_TIMEOUT'
44
44
  const timeout = await call()
45
45
  assert.equal(timeout.failure_code, 'ACTION_TIMEOUT', 'Observation timeout must not claim that the label is missing')
46
- assert.equal(timeout.evidence.dispatch_started_at_ms, null)
46
+ const retained = JSON.parse((await handleToolCall('get_interaction_diagnostics', { sessionId: timeout.diagnostics.session_id, resultId: timeout.diagnostics.result_id })).content[0].text!)
47
+ assert.equal(retained.result.evidence.dispatch_started_at_ms, null)
47
48
  assert.equal(taps.length, tapCount)
48
49
  error = undefined
49
50
 
@@ -8,6 +8,7 @@ import { withEvidence, bindSystemTrace, bindEvidenceDevice } from '../../../src/
8
8
  const parse = (r: any) => JSON.parse(r.content[0].text)
9
9
  async function run() {
10
10
  const originalLink = systemTraces.link, originalStart = systemTraces.start, originalControl = systemTraces.control
11
+ const originalCapabilities = systemTraces.iosCapabilities
11
12
  const originalApp = AndroidManage.prototype.startApp
12
13
  const id = '62a51510-c303-4a1b-af0e-f97cc976d740'
13
14
  let dispatched = 0, starts = 0
@@ -24,8 +25,16 @@ async function run() {
24
25
  }
25
26
  systemTraces.start = async () => { starts++; throw new Error('must not run') }
26
27
  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)
28
+ assert.equal(parse(ios).error.code, 'INVALID_ARGUMENT'); assert.equal(starts, 0)
29
+ assert.equal(parse(await handleToolCall('get_system_trace_capabilities', { platform: 'ios' })).error.code, 'INVALID_ARGUMENT')
30
+ for (const args of [{platform:'ios',deviceId:'sim'}, {platform:'android',appId:'fixture'}, {platform:'android',template:'Time Profiler'}, {platform:'ios',deviceId:'sim',appId:'fixture',template:'Other'}, {platform:'ios',deviceId:'sim',appId:'invalid bundle'}]) {
31
+ assert.equal(parse(await handleToolCall('start_system_trace', args)).error.code, 'INVALID_ARGUMENT')
32
+ }
33
+ systemTraces.iosCapabilities = async (deviceId, appId, template) => {
34
+ assert.equal(deviceId, 'sim'); assert.equal(appId, 'fixture'); assert.equal(template, 'Time Profiler')
35
+ return {supported: false, backend:'xctrace'} as any
36
+ }
37
+ assert.equal(parse(await handleToolCall('get_system_trace_capabilities', {platform:'ios',deviceId:'sim',appId:'fixture',template:'Time Profiler'})).backend, 'xctrace')
29
38
  for (const name of ['stop_system_trace', 'cancel_system_trace', 'get_system_trace_status']) {
30
39
  assert.equal(parse(await handleToolCall(name, { traceId: id, deviceId: 'wrong' })).error.code, 'INVALID_ARGUMENT')
31
40
  assert.equal(parse(await handleToolCall(name, { traceId: id })).error.code, 'TRACE_NOT_FOUND')
@@ -41,14 +50,16 @@ async function run() {
41
50
  }
42
51
  const result = parse(await handleToolCall('run_journey', journey))
43
52
  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)
53
+ assert.equal(result.evidence.trace_id, id)
54
+ const retained = parse(await handleToolCall('get_interaction_diagnostics', { sessionId: result.diagnostics.session_id, resultId: result.diagnostics.result_id }))
55
+ assert.equal(retained.result.evidence.trace_context.supported, true)
46
56
  assert.equal(result.evidence.correlation_id, correlation)
47
- assert.equal(result.steps[0].result.evidence.trace_context.trace_id, id)
57
+ assert.equal(result.steps[0].result.evidence.trace_id, id)
48
58
  assert.equal(result.steps[0].result.evidence.correlation_id, correlation)
49
59
  assert.equal(result.system_trace.coverage, 'covered')
50
60
  const plain = parse(await handleToolCall('run_journey', { ...journey, systemTraceId: undefined }))
51
- assert.deepEqual(plain.evidence.trace_context, { supported: false, trace_id: null, markers: [] })
61
+ const retainedPlain = parse(await handleToolCall('get_interaction_diagnostics', { sessionId: plain.diagnostics.session_id, resultId: plain.diagnostics.result_id }))
62
+ assert.deepEqual(retainedPlain.result.evidence.trace_context, { supported: false, trace_id: null, markers: [] })
52
63
  const [linked, independent] = await Promise.all([
53
64
  withEvidence({ journey: true }, async () => { bindSystemTrace(id); bindEvidenceDevice({ platform: 'android', id: 'trace-device' }); return withEvidence({ action: true }, async () => {}) }),
54
65
  withEvidence({ journey: true }, async () => {})
@@ -57,7 +68,7 @@ async function run() {
57
68
  assert.equal(independent.evidence.trace_context.trace_id, null)
58
69
  console.log('system trace tool contracts and journey evidence: PASS')
59
70
  } finally {
60
- systemTraces.link = originalLink; systemTraces.start = originalStart; systemTraces.control = originalControl
71
+ systemTraces.iosCapabilities = originalCapabilities; systemTraces.link = originalLink; systemTraces.start = originalStart; systemTraces.control = originalControl
61
72
  AndroidManage.prototype.startApp = originalApp; _resetDeviceListersForTests()
62
73
  }
63
74
  }
@@ -0,0 +1,78 @@
1
+ import assert from 'node:assert/strict'
2
+ import { mkdtemp, writeFile, rm } from 'node:fs/promises'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { probeInstallation, installationFields } from '../../../src/utils/app-installation.js'
6
+ import { listAndroidDevices } from '../../../src/utils/android/utils.js'
7
+ import { checkAndroid } from '../../../src/system/android.js'
8
+ import { ToolsManage } from '../../../src/manage/index.js'
9
+ import { runWithBudget } from '../../../src/utils/operation-budget.js'
10
+
11
+ const dir = await mkdtemp(path.join(os.tmpdir(), 'installation-test-'))
12
+ const binary = path.join(dir, 'adb')
13
+ const saved = { ...process.env }
14
+ try {
15
+ await writeFile(binary, `#!/usr/bin/env node
16
+ const args = process.argv.slice(2)
17
+ if (args[0] === '--version') console.log('Android Debug Bridge version 1.0.41')
18
+ else if (args[0] === 'devices') console.log(process.env.ONE_INSTALL_DEVICE ? 'List of devices attached\\na device' : 'List of devices attached\\na device\\nb device')
19
+ else if (args.includes('getprop')) console.log('14')
20
+ else if (args.includes('logcat')) console.log('log')
21
+ else if (args.includes('listapps')) {
22
+ if (args.at(-1) === 'failed') process.exit(1)
23
+ if (args.at(-1) === 'malformed') {console.log('not a plist'); process.exit(0)}
24
+ console.log('<?xml version="1.0"?><plist version="1.0"><dict><key>com.example.app</key><dict/></dict></plist>')
25
+ }
26
+ else {
27
+ const id = args[1], app = args.at(-1)
28
+ if (id === 'b' && args.includes('path')) process.exit(1)
29
+ if (id === 'failed') process.exit(1)
30
+ if (id === 'malformed') console.log('Error: query failed')
31
+ else if (id === 'slow') setTimeout(() => console.log('package:/late.apk'), 1000)
32
+ else if (app === 'com.example.app' && id !== 'b') console.log('package:/data/app/base.apk')
33
+ }
34
+ `, { mode: 0o755 })
35
+ process.env.ADB_PATH = binary
36
+ delete process.env.MCP_TARGET_PACKAGE
37
+ delete process.env.MCP_TARGET_APP_ID
38
+ const check = (id: string, app: string | undefined = 'com.example.app') => probeInstallation(binary, id, app, 'android-current-user')
39
+ assert.equal((await check('a')).status, 'installed')
40
+ assert.equal((await check('b')).status, 'absent')
41
+ for (const id of ['failed', 'malformed']) {
42
+ const result = await check(id)
43
+ assert.equal(result.status, 'unknown')
44
+ assert(!JSON.stringify(installationFields(result)).includes('appInstalled'))
45
+ }
46
+ assert.equal((await probeInstallation(binary, 'a', undefined, 'android-current-user')).reason, 'APP_ID_NOT_PROVIDED')
47
+ assert.equal((await check('a', 'bad;input')).checkedAt, null)
48
+ assert.equal((await probeInstallation(binary, 'a', 'com.example.app', 'android-current-user', false)).reason, 'DEVICE_UNAVAILABLE')
49
+ assert.equal((await runWithBudget(50, () => check('slow'))).status, 'unknown')
50
+ if (process.platform === 'darwin') {
51
+ assert.equal((await probeInstallation(binary, 'a', 'com.example.app', 'ios-simulator')).status, 'installed')
52
+ assert.equal((await probeInstallation(binary, 'a', 'com.missing.app', 'ios-simulator')).status, 'absent')
53
+ assert.equal((await probeInstallation(binary, 'failed', 'com.example.app', 'ios-simulator')).status, 'unknown')
54
+ assert.equal((await probeInstallation(binary, 'malformed', 'com.example.app', 'ios-simulator')).status, 'unknown')
55
+ }
56
+ const inventory = await listAndroidDevices()
57
+ assert.equal(inventory.length, 2)
58
+ assert(inventory.every(device => device.appInstallation?.reason === 'APP_ID_NOT_PROVIDED' && !Object.hasOwn(device, 'appInstalled')))
59
+ const scoped = await listAndroidDevices('com.example.app')
60
+ assert.deepEqual(scoped.map(device => device.appInstalled), [true, false])
61
+ assert.deepEqual(scoped.map(device => device.appInstallation?.deviceId), ['a', 'b'])
62
+ process.env.MCP_TARGET_PACKAGE = 'com.example.app'
63
+ const system = await checkAndroid()
64
+ assert.equal(system.appInstalled, undefined)
65
+ assert.deepEqual(system.appInstallations.map(result => result.status), ['installed', 'absent'])
66
+ assert(system.issues.some(issue => issue.includes('device b')))
67
+ process.env.ONE_INSTALL_DEVICE = '1'
68
+ assert.equal((await checkAndroid()).appInstalled, true)
69
+ process.env.MCP_TARGET_PACKAGE = 'bad;input'
70
+ const invalid = await checkAndroid()
71
+ assert.equal(invalid.appInstalled, undefined)
72
+ assert.equal(invalid.appInstallations[0].checkedAt, null)
73
+ await assert.rejects(() => ToolsManage.listDevicesHandler({ appId: 'bad;input' }), /Invalid appId/)
74
+ console.log('app installation tests passed')
75
+ } finally {
76
+ process.env = saved
77
+ await rm(dir, { recursive: true, force: true })
78
+ }
@@ -0,0 +1,36 @@
1
+ import assert from 'node:assert/strict'
2
+ import { spawnSync } from 'node:child_process'
3
+
4
+ const program = `
5
+ import assert from 'node:assert/strict';
6
+ import { withBenchmarkRequest, benchmarkStage } from './src/utils/benchmark-metrics.ts';
7
+ import { measure, withTiming } from './src/utils/timing.ts';
8
+ const values = {};
9
+ const result = await withBenchmarkRequest('outer', () => withTiming(values, async () => {
10
+ await measure('dispatch_ms', () => benchmarkStage('subprocess_ms', async () => 1));
11
+ return { unchanged: true };
12
+ }));
13
+ assert.deepEqual(result, { unchanged: true });
14
+ assert.ok(values.dispatch_ms >= 0);
15
+ await Promise.all(['first', 'second'].map(tool => withBenchmarkRequest(tool, () => benchmarkStage(tool, async () => tool))));
16
+ try { await withBenchmarkRequest('failure', async () => { throw Error('secret'); }); } catch {}
17
+ `
18
+ for (const enabled of ['0', '1']) {
19
+ const result = spawnSync(process.execPath, ['--import', 'tsx', '--input-type=module', '-e', program], {
20
+ cwd: process.cwd(), encoding: 'utf8', timeout: 15000,
21
+ env: { ...process.env, MOBILE_DEBUG_MCP_LATENCY_METRICS: enabled }
22
+ })
23
+ assert.equal(result.status, 0, result.stderr)
24
+ assert.equal(result.stdout, '')
25
+ if (enabled === '0') assert.equal(result.stderr, '')
26
+ else {
27
+ const events = result.stderr.trim().split('\n').map(line => JSON.parse(line))
28
+ assert.equal(events.length, 4)
29
+ assert.equal(new Set(events.map(event => event.request_id)).size, 4)
30
+ assert.deepEqual(events.find(event => event.tool === 'outer').spans.map((span: { stage: string }) => span.stage), ['subprocess_ms', 'dispatch_ms'])
31
+ for (const tool of ['first', 'second']) assert.deepEqual(events.find(event => event.tool === tool).spans.map((span: { stage: string }) => span.stage), [tool])
32
+ assert.equal(events.find(event => event.tool === 'failure').handler_completed, false)
33
+ assert.ok(!result.stderr.includes('secret'))
34
+ }
35
+ }
36
+ console.log('Opt-in latency metrics, failure and request isolation: PASS')
@@ -51,6 +51,17 @@ async function run() {
51
51
  }
52
52
  )
53
53
 
54
+ const base = { platform: 'android', model: 'Fixture', simulator: true }
55
+ _setDeviceListersForTests({ listAndroidDevices: async () => [
56
+ { ...base, id: 'unknown', osVersion: '17', appInstalled: true, appInstallation: {
57
+ status: 'unknown', appId: 'com.example.app', deviceId: 'unknown', scope: 'android-current-user', checkedAt: null, reason: 'CHECK_FAILED'
58
+ } },
59
+ { ...base, id: 'installed', osVersion: '16', appInstallation: {
60
+ status: 'installed', appId: 'com.example.app', deviceId: 'installed', scope: 'android-current-user', checkedAt: new Date(0).toISOString()
61
+ } }
62
+ ] })
63
+ assert.equal((await resolveTargetDevice({ platform: 'android', appId: 'com.example.app' })).id, 'installed')
64
+ assert.equal((await resolveTargetDevice({ platform: 'android', appId: 'com.other.app' })).id, 'unknown')
54
65
  console.log('resolve-device tests passed')
55
66
  } finally {
56
67
  _resetDeviceListersForTests()