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
@@ -0,0 +1,126 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import os from 'node:os'
3
+ import { getIdbCmd } from '../../../../src/utils/ios/utils.js'
4
+ import { BenchmarkSession, UnsafeMeasurement } from '../latency/end-to-end-session.js'
5
+ import { command, treeHash } from '../latency/end-to-end-environment.js'
6
+ import { summarizeAttempts, observationOverhead, type Attempt } from '../latency/end-to-end-report.js'
7
+
8
+ const deviceId = process.env.IOS_LATENCY_DEVICE_ID
9
+ if (!deviceId) throw new Error('Set IOS_LATENCY_DEVICE_ID to an explicit isolated, booted simulator')
10
+ const appId = 'com.apple.Preferences'
11
+ const smoke = process.argv.includes('--smoke')
12
+ const rounds = smoke ? 1 : 13
13
+ const warmups = smoke ? 0 : 3
14
+ const cases = ['screenshot-on', 'screenshot-off', 'tree-on', 'tree-off', 'snapshot-on', 'snapshot-off']
15
+ const attempts: Attempt[] = []
16
+ const sessions = new Map<boolean, BenchmarkSession>()
17
+ const phases: { phase: string; elapsed_ms: number; success: boolean }[] = []
18
+ const startedAt = new Date().toISOString()
19
+ const cleanup = { servers: false, settings: false }
20
+ const controller = new AbortController()
21
+ let fatal: string | undefined
22
+ let sourceStable: boolean | undefined
23
+ let metadata: Record<string, unknown> | undefined
24
+ let startedSettings = false
25
+ const root = 'docs/benchmarks/end-to-end-latency'
26
+ const file = smoke ? 'ios-smoke' : 'ios-observations'
27
+ const fingerprint = () => ({ source: treeHash('src'), build: treeHash('dist'),
28
+ shared_harness: treeHash('test/device/automated/latency'), ios_harness: treeHash('test/device/automated/latency-ios') })
29
+ const identity = fingerprint()
30
+ function idbVersion() {
31
+ try {
32
+ const interpreter = readFileSync(getIdbCmd(), 'utf8').split('\n')[0].replace(/^#!/, '').trim().split(/\s+/)
33
+ return command(interpreter[0], [...interpreter.slice(1), '-c', 'import importlib.metadata; print(importlib.metadata.version("fb-idb"))'])
34
+ } catch { return 'unavailable' }
35
+ }
36
+ async function phase<T>(name: string, operation: () => T | Promise<T>) {
37
+ const start = performance.now()
38
+ let success = false
39
+ try { const value = await operation(); success = true; return value }
40
+ finally { phases.push({ phase: name, elapsed_ms: performance.now() - start, success }) }
41
+ }
42
+ function save() {
43
+ mkdirSync(root, { recursive: true })
44
+ const summaries = summarizeAttempts(attempts)
45
+ const status = fatal ? 'stopped' : attempts.length === rounds * cases.length ? 'finished' : 'in_progress'
46
+ const outcome = fatal || attempts.some(value => !value.success) || controller.signal.aborted ? 'fail' : cleanup.servers && cleanup.settings ? 'pass' : 'pending'
47
+ writeFileSync(`${root}/${file}.json`, JSON.stringify({ status, outcome, started_at: startedAt,
48
+ performance_evidence: !smoke && sourceStable !== false, smoke, warmups, measured_attempts_per_case: smoke ? 1 : 10,
49
+ source_and_build_unchanged: sourceStable, identity, metadata, phases, fatal, cleanup,
50
+ unrun_attempts: rounds * cases.length - attempts.length,
51
+ invocation: `IOS_LATENCY_DEVICE_ID=[simulator] npx tsx test/device/automated/latency-ios/benchmark.ts${smoke ? ' --smoke' : ''}`,
52
+ summaries, observation_overhead: observationOverhead(attempts), attempts }, null, 2) + '\n')
53
+ const rows = Object.entries(summaries).map(([name, value]) => `| ${name} | ${value.total_ms?.count ?? 0}/${value.attempts} | ${value.total_ms?.p50.toFixed(1) ?? 'n/a'} | ${value.total_ms?.p95.toFixed(1) ?? 'n/a'} | ${value.failures} | ${value.warmup_failures} |`)
54
+ writeFileSync(`${root}/${file}.md`, `# iOS standalone observation ${smoke ? 'smoke (not performance evidence)' : 'baseline'}\n\nStatus: ${status}; outcome: ${outcome}. Intended screen: Settings root on an isolated simulator. No fixture journey or physical iOS claim.\n\n| Case | Successful/measured | p50 ms | p95 ms | Failures | Warm-up failures |\n| --- | ---: | ---: | ---: | ---: | ---: |\n${rows.join('\n')}\n\nSee ${file}.json for environment/build identity, all attempts, phase timings and inclusive timing diagnostics. Percentiles use the successful measured count shown for each case. With 1–19 successful samples, nearest-rank p95 is the sample maximum. Zero successful samples yield no percentiles. Failed attempts stay visible and fail the run. Probe comparisons include device variability; nested intervals cannot be summed. Application work remains in round trips. The shared #89 fixture remains required for equivalent static/network journeys.\n`)
55
+ }
56
+ async function sample(name: string, iteration: number) {
57
+ if (controller.signal.aborted) throw new UnsafeMeasurement('INTERRUPTED')
58
+ const value: Attempt = { case: name, iteration, warmup: iteration < warmups, success: false, setup_ms: 0, requests: [] }
59
+ attempts.push(value)
60
+ const session = sessions.get(!name.endsWith('-off'))!
61
+ const setupStarted = performance.now()
62
+ try {
63
+ await session.call('expect_element_visible', { platform: 'ios', selector: { text: 'Settings' }, timeout_ms: 30000, poll_interval_ms: 100 }, [])
64
+ value.setup_ms = performance.now() - setupStarted
65
+ const tool = name.startsWith('screenshot') ? 'capture_screenshot' : name.startsWith('tree') ? 'get_ui_tree' : 'capture_debug_snapshot'
66
+ await session.call(tool, { platform: 'ios', ...(tool === 'capture_debug_snapshot' ? { includeScreenshot: false, includeLogs: false } : {}) }, value.requests)
67
+ value.success = true
68
+ } catch (error) {
69
+ if (!value.setup_ms) value.setup_ms = performance.now() - setupStarted
70
+ value.failure = error instanceof UnsafeMeasurement ? error.message : value.requests.at(-1)?.failure ?? 'SETUP_FAILED'
71
+ if (error instanceof UnsafeMeasurement) throw error
72
+ } finally { save() }
73
+ }
74
+ async function main() {
75
+ const interrupt = () => { controller.abort(); for (const session of sessions.values()) session.abort() }
76
+ process.on('SIGINT', interrupt); process.on('SIGTERM', interrupt)
77
+ try {
78
+ const discovery = JSON.parse(command('xcrun', ['simctl', 'list', 'devices', 'booted', '--json']))
79
+ const devices = discovery.devices as Record<string, { udid: string; name: string; state: string }[]>
80
+ const runtime = Object.entries(devices).find(([, list]) => list.some(device => device.udid === deviceId && device.state === 'Booted'))
81
+ if (!runtime) throw new UnsafeMeasurement('BOOTED_SIMULATOR_REQUIRED')
82
+ const runtimes = JSON.parse(command('xcrun', ['simctl', 'list', 'runtimes', '--json'])).runtimes as { identifier: string; version: string; buildversion: string }[]
83
+ const runtimeInfo = runtimes.find(value => value.identifier === runtime[0])
84
+ metadata = { platform: 'ios', model: runtime[1].find(device => device.udid === deviceId)?.name,
85
+ runtime: runtime[0], os: runtimeInfo?.version, runtime_build: runtimeInfo?.buildversion,
86
+ screen: 'Settings root; English required; caller-provided isolated simulator', node: process.version,
87
+ host: { platform: os.platform(), release: os.release(), arch: os.arch() },
88
+ package_version: JSON.parse(readFileSync('package.json', 'utf8')).version,
89
+ commit: command('git', ['rev-parse', 'HEAD']), working_tree: command('git', ['status', '--porcelain', '--untracked-files=all']),
90
+ tool_versions: { xcode: command('xcodebuild', ['-version']), idb: idbVersion() },
91
+ timeouts: { setup_ms: 30000, call_ms: 300000, readiness_ms: 30000 },
92
+ journey: 'blocked: shared #89 static/network fixture unavailable', physical_ios: 'excluded' }
93
+ await phase('launch_settings', () => command('xcrun', ['simctl', 'launch', deviceId!, appId])); startedSettings = true
94
+ for (const enabled of [true, false]) {
95
+ const session = new BenchmarkSession(deviceId!, enabled)
96
+ sessions.set(enabled, session)
97
+ await phase(`server_startup_metrics_${enabled}`, () => session.connect())
98
+ await phase(`stable_screen_assertion_metrics_${enabled}`, () => session.call('expect_element_visible',
99
+ { platform: 'ios', selector: { text: 'Settings' }, timeout_ms: 30000, poll_interval_ms: 100 }, []))
100
+ if (enabled) {
101
+ const tree = await phase('screen_metadata', () => session.call('get_ui_tree', { platform: 'ios' }, []))
102
+ metadata.resolution = tree.resolution
103
+ }
104
+ }
105
+ save()
106
+ for (let iteration = 0; iteration < rounds; iteration++) for (const name of iteration % 2 ? [...cases].reverse() : cases) {
107
+ console.log(`${smoke ? 'smoke' : `round ${iteration + 1}/${rounds}`} ${name}`)
108
+ await sample(name, iteration)
109
+ }
110
+ } catch (error) { fatal = error instanceof UnsafeMeasurement ? error.message : 'PREREQUISITE_FAILED'; process.exitCode = 1 }
111
+ finally {
112
+ cleanup.servers = true
113
+ for (const session of sessions.values()) {
114
+ try { await session.close(); cleanup.servers &&= session.transport.cleanupVerified }
115
+ catch { cleanup.servers = false }
116
+ }
117
+ try { if (startedSettings) command('xcrun', ['simctl', 'terminate', deviceId!, appId]); cleanup.settings = true }
118
+ catch { cleanup.settings = false }
119
+ sourceStable = JSON.stringify(identity) === JSON.stringify(fingerprint())
120
+ if (!sourceStable) fatal = 'MEASURED_SOURCE_CHANGED'
121
+ if (!cleanup.servers || !cleanup.settings) fatal = 'CLEANUP_FAILED'
122
+ if (fatal || attempts.some(value => !value.success) || controller.signal.aborted) process.exitCode = 1
123
+ save(); process.off('SIGINT', interrupt); process.off('SIGTERM', interrupt)
124
+ }
125
+ }
126
+ main().catch(() => { console.error('iOS observation benchmark failed; inspect report'); process.exitCode = 1 })
@@ -24,9 +24,11 @@ async function run() {
24
24
  const action = parse(await call('start_app', { ...target, appId, verificationMode: 'none', responseMode: 'compact' }))
25
25
  assert.equal(action.success, true, JSON.stringify(action))
26
26
  assert.equal(action.action_id, action.evidence.action_id)
27
- assert.ok(action.evidence.dispatch_started_at_ms)
28
- assert.ok(action.evidence.dispatch_completed_at_ms >= action.evidence.dispatch_started_at_ms)
29
- assert.equal(action.evidence.verification_completed_at_ms, null)
27
+ const retained = parse(await call('get_interaction_diagnostics', { sessionId: action.diagnostics.session_id, resultId: action.diagnostics.result_id }))
28
+ assert.equal(retained.success, true)
29
+ assert.ok(retained.result.evidence.dispatch_started_at_ms)
30
+ assert.ok(retained.result.evidence.dispatch_completed_at_ms >= retained.result.evidence.dispatch_started_at_ms)
31
+ assert.equal(retained.result.evidence.verification_completed_at_ms, null)
30
32
  const actionRef = { sessionId: action.evidence.session_id, actionId: action.evidence.action_id }
31
33
  const screenResponse: any = await call('capture_screenshot', { ...target, actionRef })
32
34
  const screen = parse(screenResponse)
@@ -37,7 +39,7 @@ async function run() {
37
39
  const screenshot = screen.evidence.observations.find((v: any) => v.kind === 'screenshot')
38
40
  assert.equal(screenshot.status, 'captured')
39
41
  assert.equal(screenshot.freshness, 'fresh')
40
- assert.ok(screenshot.capture_started_at_ms >= action.evidence.dispatch_completed_at_ms)
42
+ assert.ok(screenshot.capture_started_at_ms >= retained.result.evidence.dispatch_completed_at_ms)
41
43
  const logsResponse: any = await call('get_logs', { ...target, appId, limit: 5, actionRef })
42
44
  const logs = parse(logsResponse)
43
45
  assert.equal(logs.evidence.action_id, action.action_id)
@@ -59,7 +61,7 @@ async function run() {
59
61
  assert.equal(unassociated.evidence.action_id, null)
60
62
  console.log(JSON.stringify({ status: 'PASS', server: client.getServerVersion(), platform, device: device.id,
61
63
  osVersion: device.osVersion, startingApp: appId, actionId: action.action_id,
62
- phases: { dispatchStarted: action.evidence.dispatch_started_at_ms, dispatchCompleted: action.evidence.dispatch_completed_at_ms,
64
+ phases: { dispatchStarted: retained.result.evidence.dispatch_started_at_ms, dispatchCompleted: retained.result.evidence.dispatch_completed_at_ms,
63
65
  screenshotStarted: screenshot.capture_started_at_ms, screenshotCompleted: screenshot.capture_completed_at_ms },
64
66
  screenshot: screenshot.status, logs: log.status, uiTree: tree.status,
65
67
  disabledComponents: 'omitted', foreignReference: 'unavailable',
@@ -0,0 +1,14 @@
1
+ /** Attempt every restoration step and always close the MCP transport. */
2
+ export async function cleanupReadinessDevice(restore: boolean, adb: (...args: string[]) => unknown, close: () => Promise<void>) {
3
+ const failures: unknown[] = []
4
+ try {
5
+ if (restore) {
6
+ for (const args of [['shell', 'input', 'keyevent', '224'], ['shell', 'wm', 'dismiss-keyguard'], ['shell', 'input', 'keyevent', '3']]) {
7
+ try { adb(...args) } catch (error) { failures.push(error) }
8
+ }
9
+ }
10
+ } finally {
11
+ try { await close() } catch (error) { failures.push(error) }
12
+ }
13
+ return failures
14
+ }
@@ -0,0 +1,78 @@
1
+ import { cleanupReadinessDevice } from './device-readiness-cleanup.js'
2
+ import assert from 'node:assert/strict'
3
+ import { execFileSync } from 'node:child_process'
4
+ import { writeFileSync } from 'node:fs'
5
+ import { createHash } from 'node:crypto'
6
+ import { readFileSync } from 'node:fs'
7
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
8
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
9
+
10
+ // Opt in only on a disposable emulator; never sleep the user's physical phone.
11
+ const deviceId = process.env.MCP_READINESS_DEVICE
12
+ if (!deviceId) console.log('UNRUN device readiness: set MCP_READINESS_DEVICE to a disposable emulator')
13
+ else {
14
+ assert.match(deviceId, /^emulator-\d+$/)
15
+ const adb = (...args: string[]) => execFileSync(process.env.ADB_PATH ?? 'adb', ['-s', deviceId, ...args], { encoding: 'utf8', timeout: 15000 })
16
+ const client = new Client({ name: 'device-readiness-smoke', version: '1.0.0' })
17
+ const observations: unknown[] = []
18
+ const call = async (name: string, args: Record<string, unknown> = {}) => {
19
+ const reply = await client.callTool({ name, arguments: { platform: 'android', deviceId, ...args } }, undefined, { timeout: 45000 })
20
+ const content = reply.content.find(item => item.type === 'text')
21
+ assert.ok(content?.type === 'text')
22
+ const result = JSON.parse(content.text)
23
+ observations.push({ tool: name, args, result })
24
+ return result
25
+ }
26
+ let restore = false
27
+ let journeyError: unknown
28
+ try {
29
+ await client.connect(new StdioClientTransport({ command: process.execPath, args: ['dist/server.js'], stderr: 'pipe' }))
30
+ const initial = await call('get_app_readiness')
31
+ assert.equal(initial.device_readiness.wakefulness, 'awake')
32
+ assert.equal(initial.device_readiness.lock_state, 'unlocked', 'start with an unlocked disposable emulator')
33
+ restore = true
34
+ adb('shell', 'input', 'keyevent', '223')
35
+ let sleeping = await call('get_app_readiness')
36
+ for (let poll = 0; sleeping.device_readiness.wakefulness === 'transitioning' && poll < 10; poll++) {
37
+ await new Promise(resolve => setTimeout(resolve, 100))
38
+ sleeping = await call('get_app_readiness')
39
+ }
40
+ assert.equal(sleeping.device_readiness.wakefulness, 'asleep')
41
+ assert.equal(sleeping.device_readiness.input_ready, false)
42
+ const launch = await call('start_app', { appId: 'com.android.settings', verificationMode: 'none', responseMode: 'compact', actionTimeoutMs: 10000 })
43
+ assert.equal(launch.success, true)
44
+ assert.equal(launch.device_readiness.input_ready, false, 'launch dispatch must not erase the blocker')
45
+ for (const action of [
46
+ { name: 'tap', args: { x: 10, y: 10 } },
47
+ { name: 'swipe', args: { x1: 10, y1: 10, x2: 20, y2: 20, duration: 100 } }
48
+ ]) {
49
+ const input = await call(action.name, { ...action.args, verificationMode: 'none', responseMode: 'compact', actionTimeoutMs: 10000 })
50
+ assert.equal(input.success, true)
51
+ assert.ok(input.device_readiness.blockers.includes('DEVICE_ASLEEP'), 'ordinary input retains its resolved device blocker')
52
+ }
53
+ const wait = await call('wait_for_app_ready', { timeoutMs: 1200, pollIntervalMs: 100 })
54
+ assert.equal(wait.success, false)
55
+ assert.equal(wait.failure_code, 'READINESS_TIMEOUT')
56
+ assert.ok(wait.device_readiness.blockers.includes('DEVICE_ASLEEP'), 'timeout retains the last observed blocker')
57
+ adb('shell', 'input', 'keyevent', '224')
58
+ const awake = await call('get_app_readiness')
59
+ assert.equal(awake.device_readiness.wakefulness, 'awake')
60
+ assert.ok(['locked', 'unlocked'].includes(awake.device_readiness.lock_state))
61
+ console.log(awake.device_readiness.lock_state === 'locked' ? 'PASS awake keyguard observed' : 'UNRUN real locked state: this emulator wakes unlocked; security settings were not changed')
62
+ // The default emulator has no passcode. Dismiss only its test keyguard to
63
+ // restore the explicitly required unlocked starting state.
64
+ adb('shell', 'wm', 'dismiss-keyguard')
65
+ const recovered = await call('wait_for_app_ready', { timeoutMs: 15000, stabilityMs: 100, pollIntervalMs: 100 })
66
+ assert.equal(recovered.success, true, JSON.stringify(recovered))
67
+ assert.equal(recovered.device_readiness.lock_state, 'unlocked')
68
+ assert.equal(recovered.device_readiness.input_ready, null)
69
+ } catch (error) {
70
+ journeyError = error
71
+ throw error
72
+ } finally {
73
+ const failures = await cleanupReadinessDevice(restore, adb, () => client.close())
74
+ if (process.env.MCP_READINESS_EVIDENCE) writeFileSync(process.env.MCP_READINESS_EVIDENCE, JSON.stringify({ deviceId, server: 'dist/server.js', server_sha256: createHash('sha256').update(readFileSync('dist/server.js')).digest('hex'), runtime: process.version, observations, cleanup: failures.length ? 'failed; all restoration steps and client close attempted' : 'wake, dismiss disposable keyguard, Home; no security settings changed' }, null, 2) + '\n')
75
+ if (failures.length) throw new AggregateError(journeyError ? [journeyError, ...failures] : failures, 'Device readiness cleanup failed')
76
+ }
77
+ console.log('PASS Android readiness: sleeping launch, blocked wait, awake recovery and cleanup')
78
+ }
@@ -0,0 +1,119 @@
1
+ import { verifyIosTraceShutdown } from '../../../helpers/ios-trace-shutdown.js'
2
+ import assert from 'node:assert/strict'
3
+ import { execFileSync } from 'node:child_process'
4
+ import { promises as fs } from 'node:fs'
5
+ import path from 'node:path'
6
+ import { createHash } from 'node:crypto'
7
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
8
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
9
+ import { buildReadinessFixture, readinessAppId } from '../../../fixtures/readiness-app/build.js'
10
+
11
+ const inputEnvironment = () => Object.fromEntries(['IDB_COMPANION','MCP_IDB_PATH','IDB_PATH','XCRUN_PATH'].flatMap(key=>process.env[key]?[[key,process.env[key]!]]:[]))
12
+ async function run() {
13
+ const deviceId=process.env.IOS_SYSTEM_TRACE_DEVICE
14
+ if(process.env.SKIP_DEVICE_TESTS==='1'||!deviceId){console.log('iOS system trace smoke: UNRUN (set IOS_SYSTEM_TRACE_DEVICE to an explicit simulator UDID)');return}
15
+ assert.equal(process.platform,'darwin')
16
+ const command=(args:string[])=>execFileSync('xcrun',args,{encoding:'utf8',timeout:120000,stdio:'pipe'})
17
+ const sims=JSON.parse(command(['simctl','list','devices','--json']))
18
+ const device=Object.values(sims.devices).flat().find((d:any)=>d.udid===deviceId) as any
19
+ assert.ok(device?.isAvailable,'Explicit available simulator required')
20
+ let booted=false,installed=false,fixture:ReturnType<typeof buildReadinessFixture>|undefined
21
+ const client=new Client({name:'ios-system-trace-smoke',version:'1.0.0'})
22
+ const transport=new StdioClientTransport({command:process.execPath,args:['dist/server.js'],stderr:'pipe',env:inputEnvironment()})
23
+ const owned=new Set<string>(),artifacts:string[]=[],checks:Record<string,unknown>={}
24
+ let journeyFailure: Error | undefined
25
+ const target={platform:'ios',deviceId},traceTarget={...target,appId:readinessAppId}
26
+ const call=async(name:string,args:Record<string,unknown>)=>{
27
+ const result=await client.callTool({name,arguments:args},undefined,{timeout:180000})
28
+ return JSON.parse((result.content as any[]).find(c=>c.type==='text').text)
29
+ }
30
+ const start=async(durationMs=90000)=>{
31
+ const r=await call('start_system_trace',{...traceTarget,durationMs})
32
+ if(r.trace_id&&r.cleanup?.status!=='complete')owned.add(r.trace_id)
33
+ assert.equal(r.state,'recording',JSON.stringify(r));assert.ok(r.recording_ready_at_ms>=r.capture_started_at_ms)
34
+ return r
35
+ }
36
+ const stop=async(id:string)=>{
37
+ const r=await call('stop_system_trace',{traceId:id})
38
+ if(r.artifact)artifacts.push(r.artifact.path)
39
+ assert.equal(r.state,'completed',JSON.stringify(r));assert.equal(r.cleanup.status,'complete')
40
+ assert.equal(r.artifact.validation.readable,true);owned.delete(id);return r
41
+ }
42
+ try{
43
+ if(device.state!=='Booted'){command(['simctl','boot',deviceId]);booted=true;command(['simctl','bootstatus',deviceId,'-b'])}
44
+ let preexisting=false
45
+ try{const existing=command(['simctl','get_app_container',deviceId,readinessAppId,'app']).trim();await fs.stat(existing);preexisting=true}catch{/* absent or stale simulator registration */}
46
+ assert.equal(preexisting,false,'Refuse to replace a pre-existing fixture')
47
+ fixture=buildReadinessFixture('ios');command(['simctl','install',deviceId,fixture.artifact]);installed=true
48
+ command(['simctl','launch',deviceId,readinessAppId])
49
+ await client.connect(transport)
50
+ const cap=await call('get_system_trace_capabilities',traceTarget)
51
+ assert.equal(cap.supported,true,JSON.stringify(cap));assert.equal(cap.prerequisites.permissions,'unknown')
52
+ const tree=await call('get_ui_tree',target)
53
+ assert.ok(tree.elements.some((e:any)=>e.text==='Ready'),'Fixture must be semantically visible')
54
+ assert.ok(tree.elements.some((e:any)=>e.text==='Show loader'),'Fixture button must be semantically visible')
55
+ const recording=await start()
56
+ const baselineJourney=await call('run_journey',{...target,systemTraceId:recording.trace_id,steps:[{id:'ready',type:'assert',timeoutMs:10000,assertion:{kind:'element_visible',selector:{text:'Ready'}}}]})
57
+ assert.equal(baselineJourney.success,true,JSON.stringify(baselineJourney));checks.linked_baseline='PASS; Ready asserted via built-server journey'
58
+ const journeySubmitted=Date.now()
59
+ const journey=await call('run_journey',{...target,systemTraceId:recording.trace_id,steps:[
60
+ {id:'show-loader',type:'tap',selector:{text:'Show loader'},verificationMode:'none'},
61
+ {id:'done',type:'assert',timeoutMs:30000,assertion:{kind:'element_visible',selector:{text:'Done'}}}
62
+ ]})
63
+ if (!journey.success) journeyFailure = new Error(`Traced fixture input journey failed at ${journey.stopped_at_step_id}; ${journey.steps[0].result?.failure_code}`)
64
+ else assert.equal(journey.steps[1].status,'passed')
65
+ assert.equal(journey.evidence.trace_id,recording.trace_id)
66
+ assert.equal(journey.steps[0].result.evidence.trace_id,recording.trace_id)
67
+ assert.equal(journey.system_trace.coverage,'covered');assert.ok(recording.recording_ready_at_ms<=journeySubmitted)
68
+ const completed=await stop(recording.trace_id)
69
+ assert.equal(completed.device.id,deviceId);assert.equal(completed.app_id,readinessAppId)
70
+ assert.deepEqual(completed.correlation_ids,[baselineJourney.evidence.correlation_id,journey.evidence.correlation_id]);assert.ok(completed.artifact.validation.sample_rows>0)
71
+ checks.journey={status:journey.success?'PASS':'FAIL',trace_id:recording.trace_id,correlation_id:journey.evidence.correlation_id,
72
+ ready_at_ms:recording.recording_ready_at_ms,journey_submitted_at_ms:journeySubmitted,outcome:journey.success?'Done visible via journey UI-tree assertion':journeyFailure?.message,
73
+ sample_rows:completed.artifact.validation.sample_rows,artifact_bytes:completed.artifact.size_bytes,warnings:completed.warnings}
74
+ const stale=await call('run_journey',{...target,systemTraceId:recording.trace_id,steps:[{id:'must-not-tap',type:'tap',selector:{text:'Show loader'},verificationMode:'none'}]})
75
+ assert.equal(stale.error.code,'TRACE_NOT_FOUND')
76
+ const unchanged=await call('expect_element_visible',{...target,selector:{text:journey.success?'Done':'Ready'},timeoutMs:10000})
77
+ assert.ok(unchanged.success||unchanged.visible||unchanged.status==='success',JSON.stringify(unchanged))
78
+ checks.stale_id='PASS; rejected tap left prior fixture label visible'
79
+ const cancelled=await start(30000),directory=cancelled.cleanup.remaining_resources[0]
80
+ const cancel=await call('cancel_system_trace',{traceId:cancelled.trace_id})
81
+ assert.equal(cancel.state,'cancelled');assert.equal(cancel.cleanup.status,'complete');assert.equal(cancel.artifact,null);owned.delete(cancelled.trace_id)
82
+ await assert.rejects(()=>fs.stat(directory),{code:'ENOENT'});checks.cancel='PASS'
83
+ const short=await start(5000)
84
+ let expired=short
85
+ for(let i=0;i<90&&expired.state==='recording';i++){
86
+ await new Promise(r=>setTimeout(r,500));expired=await call('get_system_trace_status',{traceId:short.trace_id})
87
+ }
88
+ if(expired.artifact)artifacts.push(expired.artifact.path)
89
+ assert.equal(expired.state,'completed',JSON.stringify(expired));assert.equal(expired.completion_reason,'duration_limit');assert.equal(expired.cleanup.status,'complete');owned.delete(short.trace_id);checks.expiry='PASS'
90
+ const failedRecording=await start(60000)
91
+ const failedJourney=await call('run_journey',{...target,systemTraceId:failedRecording.trace_id,steps:[{id:'missing',type:'assert',timeoutMs:1000,assertion:{kind:'element_visible',selector:{text:'impossible-trace-fixture-label'}}}]})
92
+ assert.equal(failedJourney.success,false)
93
+ assert.equal((await call('get_system_trace_status',{traceId:failedRecording.trace_id})).state,'recording')
94
+ await stop(failedRecording.trace_id);checks.failed_journey='PASS; failure retained capture for explicit stop'
95
+ await verifyIosTraceShutdown(traceTarget,inputEnvironment());checks.shutdown='PASS; owned recording and partial directory removed'
96
+ const live=command(['simctl','spawn',deviceId,'launchctl','list'])
97
+ assert.ok(live.includes(`UIKitApplication:${readinessAppId}[`),'Recorder cleanup must leave the fixture app running')
98
+ checks.app_survived_cleanup='PASS'
99
+ const hash=createHash('sha256')
100
+ for(const file of ['dist/server.js','dist/observe/system-trace.js','dist/observe/system-trace-router.js','dist/observe/ios-trace/native.js','dist/observe/ios-trace/sessions.js','dist/observe/ios-trace/process.js'])hash.update(await fs.readFile(file))
101
+ checks.status=journeyFailure?'FAIL':'PASS';checks.build_sha256=hash.digest('hex');checks.runtime=process.version;checks.device=deviceId;checks.server=client.getServerVersion()
102
+ checks.physical_device='UNRUN; unsupported initial matrix';checks.native_failure_injection='UNRUN on real target; deterministic unit tests separate'
103
+ checks.input_toolchain=process.env.IDB_COMPANION?'Explicit test companion via IDB_COMPANION; see investigation.md':'Repository default idb companion'
104
+ if(journeyFailure) throw journeyFailure
105
+ }catch(error){checks.status='FAIL';checks.error=String(error);throw error}finally{
106
+ const errors:string[]=[]
107
+ for(const id of owned)try{const r=await call('cancel_system_trace',{traceId:id});if(r.artifact&&!artifacts.includes(r.artifact.path))artifacts.push(r.artifact.path);if(r.cleanup?.status!=='complete')errors.push(id)}catch{errors.push(id)}
108
+ await client.close()
109
+ for(const file of artifacts)await fs.rm(path.dirname(file),{recursive:true,force:true})
110
+ if(installed)command(['simctl','uninstall',deviceId,readinessAppId])
111
+ if(fixture)await fs.rm(fixture.directory,{recursive:true,force:true})
112
+ if(booted)command(['simctl','shutdown',deviceId])
113
+ checks.cleanup={trace_errors:errors,fixture_removed:installed,simulator_shutdown:booted,host_artifacts_removed:true}
114
+ console.log(JSON.stringify(checks))
115
+ if(process.env.IOS_SYSTEM_TRACE_EVIDENCE_PATH)await fs.writeFile(process.env.IOS_SYSTEM_TRACE_EVIDENCE_PATH,JSON.stringify(checks,null,2)+'\n')
116
+ assert.deepEqual(errors,[],'Trace cleanup failed')
117
+ }
118
+ }
119
+ run().catch(error=>{console.error(error);process.exitCode=1})
@@ -39,8 +39,8 @@ async function run() {
39
39
  ] })
40
40
  assert.equal(journey.success, true, JSON.stringify(journey))
41
41
  assert.equal(journey.steps[1].status, 'passed', 'The baseline Settings-owned UI element must remain semantically visible')
42
- assert.equal(journey.evidence.trace_context.trace_id, start.trace_id)
43
- assert.equal(journey.steps[0].result.evidence.trace_context.trace_id, start.trace_id)
42
+ assert.equal(journey.evidence.trace_id, start.trace_id)
43
+ assert.equal(journey.steps[0].result.evidence.trace_id, start.trace_id)
44
44
  assert.equal(journey.system_trace.coverage, 'covered')
45
45
  const stop = await call('stop_system_trace', { traceId: start.trace_id })
46
46
  if (stop.artifact) artifacts.push(stop.artifact.path)