agent-device 0.18.3 → 0.19.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 (215) hide show
  1. package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.18.3.apk → agent-device-android-multitouch-helper-0.19.0.apk} +0 -0
  2. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.0.apk.sha256 +1 -0
  3. package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.18.3.manifest.json → agent-device-android-multitouch-helper-0.19.0.manifest.json} +4 -4
  4. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.18.3.apk → agent-device-android-snapshot-helper-0.19.0.apk} +0 -0
  5. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.0.apk.sha256 +1 -0
  6. package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.18.3.manifest.json → agent-device-android-snapshot-helper-0.19.0.manifest.json} +6 -6
  7. package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTapPointPolicy.swift +28 -0
  8. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +9 -120
  9. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +132 -66
  10. package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +155 -0
  11. package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +67 -0
  12. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +20 -74
  13. package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +64 -0
  14. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +0 -177
  15. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +81 -48
  16. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +102 -129
  17. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +208 -46
  18. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +82 -11
  19. package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +72 -0
  20. package/dist/src/agent-browser-tool.js +1 -0
  21. package/dist/src/android-adb.d.ts +64 -234
  22. package/dist/src/android-adb.js +1 -1
  23. package/dist/src/android.js +1 -5
  24. package/dist/src/app-lifecycle.js +7 -0
  25. package/dist/src/apple-runner-options.js +1 -0
  26. package/dist/src/apps.js +5 -11
  27. package/dist/src/artifacts.d.ts +4 -3
  28. package/dist/src/artifacts.js +1 -1
  29. package/dist/src/audio-probe.js +1 -1
  30. package/dist/src/batch.d.ts +2 -177
  31. package/dist/src/batch.js +1 -1
  32. package/dist/src/cli-help.js +58 -37
  33. package/dist/src/cli-output.js +1 -1
  34. package/dist/src/cli-runner.js +2 -0
  35. package/dist/src/cli.js +12 -41
  36. package/dist/src/client-metro-companion.js +1 -0
  37. package/dist/src/client.js +3 -0
  38. package/dist/src/command-metadata.js +1 -0
  39. package/dist/src/command-schema.js +22 -0
  40. package/dist/src/command-surface.js +1 -1
  41. package/dist/src/contracts.d.ts +2 -213
  42. package/dist/src/contracts.js +1 -1
  43. package/dist/src/debug-symbols.js +5 -3
  44. package/dist/src/device-input-state.js +2 -0
  45. package/dist/src/device-rotation.js +14 -0
  46. package/dist/src/devices.js +1 -1
  47. package/dist/src/devices2.js +1 -0
  48. package/dist/src/devices3.js +1 -0
  49. package/dist/src/find.js +1 -1
  50. package/dist/src/finders.d.ts +2 -81
  51. package/dist/src/finders.js +1 -1
  52. package/dist/src/generic.js +1 -1
  53. package/dist/src/handler-utils.js +1 -1
  54. package/dist/src/helper.js +2 -0
  55. package/dist/src/host-process.js +2 -0
  56. package/dist/src/index.d.ts +1265 -1786
  57. package/dist/src/index.js +1 -1
  58. package/dist/src/input-actions.js +3 -1
  59. package/dist/src/input-actions2.js +1 -0
  60. package/dist/src/install-source.d.ts +13 -14
  61. package/dist/src/install-source.js +1 -1
  62. package/dist/src/install-source2.js +1 -0
  63. package/dist/src/interaction-snapshot.js +1 -0
  64. package/dist/src/interaction.js +1 -1
  65. package/dist/src/interactor.js +2 -1
  66. package/dist/src/internal/bin.d.ts +1 -1
  67. package/dist/src/internal/bin.js +2 -5
  68. package/dist/src/internal/companion-tunnel.d.ts +1 -1
  69. package/dist/src/internal/companion-tunnel.js +1 -1
  70. package/dist/src/internal/daemon.d.ts +1 -1
  71. package/dist/src/internal/daemon.js +16 -1
  72. package/dist/src/internal/png-worker.d.ts +29 -26
  73. package/dist/src/internal/png-worker.js +1 -1
  74. package/dist/src/internal/update-check-entry.d.ts +1 -1
  75. package/dist/src/internal/update-check-entry.js +1 -1
  76. package/dist/src/io.d.ts +2 -85
  77. package/dist/src/io.js +1 -1
  78. package/dist/src/lease.js +1 -1
  79. package/dist/src/linux.js +1 -1
  80. package/dist/src/logcat.js +6 -0
  81. package/dist/src/manifest.js +1 -0
  82. package/dist/src/metro.d.ts +70 -113
  83. package/dist/src/metro.js +1 -1
  84. package/dist/src/metro2.js +1 -0
  85. package/dist/src/notifications.js +1 -1
  86. package/dist/src/output-path.js +1 -0
  87. package/dist/src/path-resolution.js +1 -0
  88. package/dist/src/png-worker-contract.js +1 -0
  89. package/dist/src/points.js +4 -0
  90. package/dist/src/project-runtime.js +1 -0
  91. package/dist/src/provider.js +1 -1
  92. package/dist/src/providers.js +1 -0
  93. package/dist/src/react-native.js +1 -1
  94. package/dist/src/record-trace-recording.js +2 -5
  95. package/dist/src/record-trace.js +1 -1
  96. package/dist/src/recording-provider.js +1 -1
  97. package/dist/src/registry.js +37 -0
  98. package/dist/src/remote-config.d.ts +2 -83
  99. package/dist/src/reporting.js +9 -0
  100. package/dist/src/rolldown-runtime.js +1 -0
  101. package/dist/src/runner-client.js +1 -0
  102. package/dist/src/runner-provider.js +1 -0
  103. package/dist/src/runner-sequence.js +1 -1
  104. package/dist/src/runtime-transport.js +1 -0
  105. package/dist/src/runtime.js +3 -0
  106. package/dist/src/scoped-provider.js +1 -0
  107. package/dist/src/sdk-android-adb.d.ts +144 -0
  108. package/dist/src/sdk-android-adb.js +4 -0
  109. package/dist/src/sdk-app-inventory.d.ts +4 -0
  110. package/dist/src/sdk-app-inventory.js +1 -0
  111. package/dist/src/sdk-batch-runner.d.ts +40 -0
  112. package/dist/src/sdk-batch-runner.js +2 -0
  113. package/dist/src/sdk-batch.js +2 -0
  114. package/dist/src/sdk-contracts.d.ts +386 -0
  115. package/dist/src/sdk-contracts.js +2 -0
  116. package/dist/src/sdk-finders.d.ts +61 -0
  117. package/dist/src/sdk-finders.js +1 -0
  118. package/dist/src/sdk-io.d.ts +79 -0
  119. package/dist/src/sdk-io.js +1 -0
  120. package/dist/src/sdk-remote-config.d.ts +140 -0
  121. package/dist/src/sdk-remote-config.js +1 -0
  122. package/dist/src/sdk-selectors.js +1 -0
  123. package/dist/src/selector-runtime.js +1 -1
  124. package/dist/src/selectors.d.ts +39 -137
  125. package/dist/src/selectors.js +1 -1
  126. package/dist/src/selectors2.js +1 -0
  127. package/dist/src/server.js +3 -2
  128. package/dist/src/session.js +10 -11
  129. package/dist/src/setting-state.js +1 -0
  130. package/dist/src/simctl.js +1 -0
  131. package/dist/src/simulator.js +1 -2
  132. package/dist/src/snapshot-diagnostics.js +3 -0
  133. package/dist/src/snapshot-helper.js +5 -0
  134. package/dist/src/snapshot-quality.js +1 -0
  135. package/dist/src/snapshot.js +1 -2
  136. package/dist/src/snapshot2.js +2 -0
  137. package/dist/src/{5560.js → timeouts.js} +1 -1
  138. package/dist/src/tool-provider.js +3 -0
  139. package/dist/src/tool-provider2.js +1 -0
  140. package/dist/src/ttl-memo.js +1 -0
  141. package/dist/src/update-check.js +1 -0
  142. package/dist/src/url.js +1 -0
  143. package/dist/src/version.js +1 -0
  144. package/dist/src/web.js +1 -1
  145. package/dist/src/xml.js +2 -0
  146. package/package.json +12 -19
  147. package/server.json +2 -2
  148. package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.18.3.apk.sha256 +0 -1
  149. package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.18.3.apk.sha256 +0 -1
  150. package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +0 -443
  151. package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +0 -169
  152. package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +0 -220
  153. package/dist/src/1404.js +0 -1
  154. package/dist/src/1986.js +0 -1
  155. package/dist/src/2748.js +0 -1
  156. package/dist/src/2948.js +0 -30
  157. package/dist/src/3006.js +0 -1
  158. package/dist/src/314.js +0 -1
  159. package/dist/src/3340.js +0 -8
  160. package/dist/src/4418.js +0 -1
  161. package/dist/src/4428.js +0 -3
  162. package/dist/src/485.js +0 -1
  163. package/dist/src/4917.js +0 -1
  164. package/dist/src/495.js +0 -5
  165. package/dist/src/5448.js +0 -3
  166. package/dist/src/5484.js +0 -1
  167. package/dist/src/5643.js +0 -1
  168. package/dist/src/587.js +0 -1
  169. package/dist/src/5913.js +0 -4
  170. package/dist/src/6849.js +0 -1
  171. package/dist/src/6918.js +0 -1
  172. package/dist/src/6967.js +0 -1
  173. package/dist/src/760.js +0 -1
  174. package/dist/src/7871.js +0 -1
  175. package/dist/src/8184.js +0 -1
  176. package/dist/src/8225.js +0 -1
  177. package/dist/src/8391.js +0 -1
  178. package/dist/src/8404.js +0 -2
  179. package/dist/src/8414.js +0 -2
  180. package/dist/src/8875.js +0 -4
  181. package/dist/src/893.js +0 -1
  182. package/dist/src/8936.js +0 -1
  183. package/dist/src/9414.js +0 -1
  184. package/dist/src/9581.js +0 -22
  185. package/dist/src/9612.js +0 -1
  186. package/dist/src/9624.js +0 -1
  187. package/dist/src/9722.js +0 -39
  188. package/dist/src/devices~1.js +0 -1
  189. package/dist/src/devices~2.js +0 -1
  190. package/dist/src/input-actions~1.js +0 -1
  191. package/dist/src/rslib-runtime.js +0 -1
  192. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +0 -0
  193. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj +0 -0
  194. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
  195. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcshareddata/xcschemes/AgentDeviceRunner.xcscheme +0 -0
  196. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +0 -0
  197. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests.entitlements +0 -0
  198. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +0 -0
  199. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +0 -0
  200. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.h +0 -0
  201. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.m +0 -0
  202. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +0 -0
  203. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +0 -0
  204. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +0 -0
  205. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Environment.swift +0 -0
  206. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Exceptions.swift +0 -0
  207. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +0 -0
  208. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +0 -0
  209. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift +0 -0
  210. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +0 -0
  211. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TvRemote.swift +0 -0
  212. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests.xctestplan +0 -0
  213. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-overlay.swift +0 -0
  214. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-resize.swift +0 -0
  215. /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-trim.swift +0 -0
@@ -45,6 +45,11 @@ extension RunnerTests {
45
45
  }
46
46
  }
47
47
 
48
+ struct TextEntryStabilization {
49
+ let element: XCUIElement?
50
+ let focusConfirmed: Bool
51
+ }
52
+
48
53
  func clearTextInput(_ element: XCUIElement) {
49
54
  // Skip the clear (delete burst + moveCaretToEnd edge-tap) ONLY when we can confirm the
50
55
  // field is empty. Why skip: the edge-tap computes a point from the element frame, which can
@@ -90,30 +95,45 @@ extension RunnerTests {
90
95
  #endif
91
96
  }
92
97
 
93
- func stabilizeTextInputBeforeTyping(app: XCUIApplication, target: XCUIElement?) -> XCUIElement? {
98
+ func stabilizeTextInputBeforeTyping(
99
+ app: XCUIApplication,
100
+ target: XCUIElement?,
101
+ keyboardVisibleBeforeTap: Bool? = nil
102
+ ) -> TextEntryStabilization {
94
103
  #if os(tvOS)
95
- return target
104
+ return TextEntryStabilization(element: target, focusConfirmed: true)
96
105
  #else
97
106
  let latest = target
98
- let keyboardVisibleAtEntry = isKeyboardVisible(app: app)
107
+ let keyboardVisibleAtEntry = keyboardVisibleBeforeTap ?? isKeyboardVisible(app: app)
99
108
  let deadline = Date().addingTimeInterval(TextEntryTiming.focusTimeout)
100
109
  while Date() < deadline {
101
110
  if let focused = focusedTextInput(app: app) {
102
- return focused
111
+ return TextEntryStabilization(element: focused, focusConfirmed: true)
103
112
  }
104
113
  // focusedTextInput is intentionally nil on iOS; treat the keyboard transitioning to
105
114
  // visible after our tap as the focus-moved signal. Don't fast-path when it was already up.
106
115
  if keyboardBecameVisible(app: app, wasVisibleAtEntry: keyboardVisibleAtEntry) {
107
- return latest
116
+ return TextEntryStabilization(element: latest, focusConfirmed: true)
108
117
  }
109
118
  sleepFor(TextEntryTiming.pollInterval)
110
119
  }
111
- return latest
120
+ return TextEntryStabilization(element: latest, focusConfirmed: false)
112
121
  #endif
113
122
  }
114
123
 
115
124
  func focusTextInputForTextEntry(app: XCUIApplication, x: Double?, y: Double?) -> TextEntryTarget {
116
125
  guard let x, let y else {
126
+ // Bare `type` targets the current first responder. On iOS we intentionally do not trust
127
+ // `hasKeyboardFocus`, but an already-visible software keyboard is sufficient evidence that
128
+ // app.typeText has a receiver; waiting the full readiness timeout cannot prove a stronger
129
+ // target because there is no selector/coordinate focus move to validate.
130
+ if isKeyboardVisible(app: app) {
131
+ return TextEntryTarget(
132
+ element: focusedTextInput(app: app),
133
+ refreshPoint: nil,
134
+ prefersFocusedElement: true
135
+ )
136
+ }
117
137
  let focused = waitForTextEntryReadiness(
118
138
  app: app,
119
139
  target: TextEntryTarget(
@@ -125,6 +145,7 @@ extension RunnerTests {
125
145
  return TextEntryTarget(element: focused, refreshPoint: nil, prefersFocusedElement: true)
126
146
  }
127
147
 
148
+ let keyboardVisibleBeforeTap = isKeyboardVisible(app: app)
128
149
  let target = textInputAt(app: app, x: x, y: y)
129
150
  let requestedPoint = CGPoint(x: x, y: y)
130
151
  if let target {
@@ -137,15 +158,30 @@ extension RunnerTests {
137
158
  } else {
138
159
  _ = tapAt(app: app, x: x, y: y)
139
160
  }
140
- let stabilized = stabilizeTextInputBeforeTyping(app: app, target: target)
141
- let element = waitForTextEntryReadiness(
142
- app: app,
143
- target: TextEntryTarget(
144
- element: stabilized ?? target,
145
- refreshPoint: requestedPoint,
161
+ // A visible keyboard is not enough evidence for app.typeText, because focus may still
162
+ // belong to a previous field. With a concrete target we type through XCUIElement.typeText,
163
+ // so after tapping it the iOS readiness timeout cannot prove anything stronger.
164
+ if keyboardVisibleBeforeTap, let target {
165
+ return TextEntryTarget(
166
+ element: target,
167
+ refreshPoint: textEntryRefreshPoint(for: target) ?? requestedPoint,
146
168
  prefersFocusedElement: false
147
169
  )
148
- ) ?? stabilized ?? target
170
+ }
171
+ let stabilized = stabilizeTextInputBeforeTyping(
172
+ app: app,
173
+ target: target,
174
+ keyboardVisibleBeforeTap: keyboardVisibleBeforeTap
175
+ )
176
+ let readyTarget = TextEntryTarget(
177
+ element: stabilized.element ?? target,
178
+ refreshPoint: requestedPoint,
179
+ prefersFocusedElement: false
180
+ )
181
+ let concreteTargetReady = keyboardVisibleBeforeTap && readyTarget.element != nil
182
+ let element = stabilized.focusConfirmed || concreteTargetReady
183
+ ? (stabilized.element ?? target)
184
+ : (waitForTextEntryReadiness(app: app, target: readyTarget) ?? stabilized.element ?? target)
149
185
  return TextEntryTarget(
150
186
  element: element,
151
187
  refreshPoint: textEntryRefreshPoint(for: element) ?? requestedPoint,
@@ -155,18 +191,32 @@ extension RunnerTests {
155
191
 
156
192
  func focusTextInputForTextEntry(app: XCUIApplication, element: XCUIElement) -> TextEntryTarget {
157
193
  let point = textEntryRefreshPoint(for: element)
194
+ let keyboardVisibleBeforeTap = isKeyboardVisible(app: app)
158
195
  if let point {
159
196
  _ = tapAt(app: app, x: point.x, y: point.y)
160
197
  }
161
- let stabilized = stabilizeTextInputBeforeTyping(app: app, target: element)
162
- let resolved = waitForTextEntryReadiness(
163
- app: app,
164
- target: TextEntryTarget(
165
- element: stabilized ?? element,
166
- refreshPoint: point,
198
+ // See the coordinate-target path above: direct element typing keeps this scoped to the
199
+ // tapped target, while the first-character warmup and final verify still catch dropped input.
200
+ if keyboardVisibleBeforeTap {
201
+ return TextEntryTarget(
202
+ element: element,
203
+ refreshPoint: textEntryRefreshPoint(for: element) ?? point,
167
204
  prefersFocusedElement: false
168
205
  )
169
- ) ?? stabilized ?? element
206
+ }
207
+ let stabilized = stabilizeTextInputBeforeTyping(
208
+ app: app,
209
+ target: element,
210
+ keyboardVisibleBeforeTap: keyboardVisibleBeforeTap
211
+ )
212
+ let readyTarget = TextEntryTarget(
213
+ element: stabilized.element ?? element,
214
+ refreshPoint: point,
215
+ prefersFocusedElement: false
216
+ )
217
+ let resolved = stabilized.focusConfirmed
218
+ ? (stabilized.element ?? element)
219
+ : (waitForTextEntryReadiness(app: app, target: readyTarget) ?? stabilized.element ?? element)
170
220
  return TextEntryTarget(
171
221
  element: resolved,
172
222
  refreshPoint: textEntryRefreshPoint(for: resolved) ?? point,
@@ -199,31 +249,51 @@ extension RunnerTests {
199
249
  target: TextEntryTarget,
200
250
  text: String,
201
251
  delaySeconds: Double,
202
- repairMode: TextTypingRepairMode = .none
252
+ repairMode: TextTypingRepairMode = .none,
253
+ commandId: String? = nil
203
254
  ) -> TextEntryResult {
255
+ let totalStartedAt = Date()
204
256
  guard !text.isEmpty else {
257
+ logTextEntryPhase(commandId: commandId, phase: "total", startedAt: totalStartedAt, chars: 0, mode: repairMode)
205
258
  return TextEntryResult(verified: true, repaired: false, expectedText: "", observedText: "")
206
259
  }
207
260
  var activeTarget = target
261
+ let initialResolveStartedAt = Date()
208
262
  let initialTarget = resolveTextEntryElement(app: app, target: activeTarget)
209
263
  activeTarget = activeTarget.withElement(initialTarget)
210
264
  let currentText = editableTextValue(for: initialTarget, treatingPlaceholderAsEmpty: true)
211
265
  let initialText = repairMode == .append ? currentText : nil
212
266
  let expectedText = expectedTextEntryValue(typedText: text, mode: repairMode, initialText: initialText)
267
+ logTextEntryPhase(
268
+ commandId: commandId,
269
+ phase: "initial-resolve",
270
+ startedAt: initialResolveStartedAt,
271
+ chars: text.count,
272
+ mode: repairMode
273
+ )
213
274
 
214
275
  if repairMode == .replacement {
215
276
  guard let replacementTarget = initialTarget else {
277
+ logTextEntryPhase(commandId: commandId, phase: "total", startedAt: totalStartedAt, chars: text.count, mode: repairMode)
216
278
  return TextEntryResult(verified: nil, repaired: false, expectedText: expectedText, observedText: nil)
217
279
  }
218
280
  if currentText == nil || currentText?.isEmpty == false {
281
+ let clearStartedAt = Date()
219
282
  clearTextInput(replacementTarget)
220
283
  activeTarget = activeTarget.withElement(replacementTarget)
284
+ logTextEntryPhase(
285
+ commandId: commandId,
286
+ phase: "clear",
287
+ startedAt: clearStartedAt,
288
+ chars: text.count,
289
+ mode: repairMode
290
+ )
221
291
  }
222
292
  }
223
293
 
224
294
  func typeIntoCurrentTarget(_ value: String) -> XCUIElement? {
225
295
  if let currentTarget = resolveTextEntryElement(app: app, target: activeTarget) {
226
- app.typeText(value)
296
+ currentTarget.typeText(value)
227
297
  return currentTarget
228
298
  } else {
229
299
  app.typeText(value)
@@ -248,76 +318,158 @@ extension RunnerTests {
248
318
  let characters = Array(text)
249
319
  if delaySeconds > 0 && characters.count > 1 {
250
320
  var typedTarget: XCUIElement?
321
+ let delayedTypeStartedAt = Date()
251
322
  for (index, character) in characters.enumerated() {
252
323
  typedTarget = typeIntoCurrentTarget(String(character)) ?? typedTarget
253
324
  if index + 1 < characters.count {
254
325
  sleepFor(delaySeconds)
255
326
  }
256
327
  }
328
+ logTextEntryPhase(
329
+ commandId: commandId,
330
+ phase: "type-delayed",
331
+ startedAt: delayedTypeStartedAt,
332
+ chars: characters.count,
333
+ mode: repairMode
334
+ )
257
335
  if repairMode == .none {
336
+ logTextEntryPhase(commandId: commandId, phase: "total", startedAt: totalStartedAt, chars: text.count, mode: repairMode)
258
337
  return TextEntryResult(verified: nil, repaired: false, expectedText: nil, observedText: nil)
259
338
  }
260
- let repairResult = repairTextEntryIfNeeded(
339
+ let verifyStartedAt = Date()
340
+ let result = verifyTextEntryWithRepairIfNeeded(
261
341
  app: app,
262
342
  target: activeTarget.withElement(typedTarget),
263
343
  expectedText: expectedText,
264
344
  repairMode: repairMode
265
345
  )
266
- return verifyTextEntry(
267
- app: app,
268
- target: activeTarget.withElement(typedTarget),
269
- expectedText: expectedText,
270
- repaired: repairResult.repaired
346
+ logTextEntryPhase(
347
+ commandId: commandId,
348
+ phase: "verify",
349
+ startedAt: verifyStartedAt,
350
+ chars: characters.count,
351
+ mode: repairMode
271
352
  )
353
+ logTextEntryPhase(commandId: commandId, phase: "total", startedAt: totalStartedAt, chars: text.count, mode: repairMode)
354
+ return result
272
355
  }
273
356
 
274
357
  let typedTarget: XCUIElement?
275
358
  if repairMode != .none && characters.count > 1 {
276
359
  let firstCharacter = String(characters[0])
360
+ let firstStartedAt = Date()
277
361
  var firstTypedTarget = typeIntoCurrentTarget(firstCharacter)
362
+ logTextEntryPhase(
363
+ commandId: commandId,
364
+ phase: "type-first",
365
+ startedAt: firstStartedAt,
366
+ chars: 1,
367
+ mode: repairMode
368
+ )
278
369
  activeTarget = activeTarget.withElement(firstTypedTarget)
279
370
  let warmupExpectedText = expectedTextEntryValue(
280
371
  typedText: firstCharacter,
281
372
  mode: repairMode,
282
373
  initialText: initialText
283
374
  )
375
+ let warmupStartedAt = Date()
284
376
  waitForWarmupValue(warmupExpectedText, target: activeTarget)
377
+ logTextEntryPhase(
378
+ commandId: commandId,
379
+ phase: "warmup",
380
+ startedAt: warmupStartedAt,
381
+ chars: 1,
382
+ mode: repairMode
383
+ )
285
384
  let remainingText = String(characters.dropFirst())
385
+ let remainingStartedAt = Date()
286
386
  firstTypedTarget = typeIntoCurrentTarget(remainingText) ?? firstTypedTarget
387
+ logTextEntryPhase(
388
+ commandId: commandId,
389
+ phase: "type-remaining",
390
+ startedAt: remainingStartedAt,
391
+ chars: characters.count - 1,
392
+ mode: repairMode
393
+ )
287
394
  typedTarget = firstTypedTarget
288
395
  } else {
396
+ let typeStartedAt = Date()
289
397
  typedTarget = typeIntoCurrentTarget(text)
398
+ logTextEntryPhase(
399
+ commandId: commandId,
400
+ phase: "type-all",
401
+ startedAt: typeStartedAt,
402
+ chars: characters.count,
403
+ mode: repairMode
404
+ )
290
405
  }
291
406
  if repairMode == .none {
407
+ logTextEntryPhase(commandId: commandId, phase: "total", startedAt: totalStartedAt, chars: text.count, mode: repairMode)
292
408
  return TextEntryResult(verified: nil, repaired: false, expectedText: nil, observedText: nil)
293
409
  }
294
- let repairResult = repairTextEntryIfNeeded(
410
+ let verifyStartedAt = Date()
411
+ let result = verifyTextEntryWithRepairIfNeeded(
295
412
  app: app,
296
413
  target: activeTarget.withElement(typedTarget),
297
414
  expectedText: expectedText,
298
415
  repairMode: repairMode
299
416
  )
300
- return verifyTextEntry(
301
- app: app,
302
- target: activeTarget.withElement(typedTarget),
303
- expectedText: expectedText,
304
- repaired: repairResult.repaired
417
+ logTextEntryPhase(
418
+ commandId: commandId,
419
+ phase: "verify",
420
+ startedAt: verifyStartedAt,
421
+ chars: characters.count,
422
+ mode: repairMode
305
423
  )
424
+ logTextEntryPhase(commandId: commandId, phase: "total", startedAt: totalStartedAt, chars: text.count, mode: repairMode)
425
+ return result
426
+ }
427
+
428
+ private func logTextEntryPhase(
429
+ commandId: String?,
430
+ phase: String,
431
+ startedAt: Date,
432
+ chars: Int,
433
+ mode: TextTypingRepairMode
434
+ ) {
435
+ NSLog(
436
+ "AGENT_DEVICE_RUNNER_TEXT_ENTRY_PHASE commandId=%@ phase=%@ durationMs=%.1f chars=%d mode=%@",
437
+ commandId ?? "",
438
+ phase,
439
+ Date().timeIntervalSince(startedAt) * 1000.0,
440
+ chars,
441
+ textEntryModeName(mode)
442
+ )
443
+ }
444
+
445
+ func textEntryModeName(_ mode: TextTypingRepairMode) -> String {
446
+ switch mode {
447
+ case .none:
448
+ return "none"
449
+ case .append:
450
+ return "append"
451
+ case .replacement:
452
+ return "replacement"
453
+ }
306
454
  }
307
455
 
308
- private func repairTextEntryIfNeeded(
456
+ private func verifyTextEntryWithRepairIfNeeded(
309
457
  app: XCUIApplication,
310
458
  target: TextEntryTarget,
311
459
  expectedText: String?,
312
460
  repairMode: TextTypingRepairMode
313
461
  ) -> TextEntryResult {
462
+ let initialResult = verifyTextEntry(
463
+ app: app,
464
+ target: target,
465
+ expectedText: expectedText,
466
+ repaired: false
467
+ )
314
468
  #if os(iOS)
315
- guard let targetElement = resolveTextEntryElement(app: app, target: target) else {
316
- return TextEntryResult(verified: nil, repaired: false, expectedText: expectedText, observedText: nil)
317
- }
318
- guard let expectedText else {
319
- let observedText = editableTextValue(for: targetElement)
320
- return TextEntryResult(verified: nil, repaired: false, expectedText: nil, observedText: observedText)
469
+ guard initialResult.verified == false,
470
+ let expectedText = initialResult.expectedText
471
+ else {
472
+ return initialResult
321
473
  }
322
474
  guard shouldRepairTextEntry(
323
475
  app: app,
@@ -325,11 +477,16 @@ extension RunnerTests {
325
477
  expectedText: expectedText,
326
478
  repairMode: repairMode
327
479
  ) else {
328
- return verifyTextEntry(app: app, target: target, expectedText: expectedText, repaired: false)
480
+ return verifyTextEntry(
481
+ app: app,
482
+ target: target,
483
+ expectedText: expectedText,
484
+ repaired: false
485
+ )
329
486
  }
330
487
 
331
488
  guard let repairTarget = resolveTextEntryElement(app: app, target: target) else {
332
- return TextEntryResult(verified: nil, repaired: false, expectedText: expectedText, observedText: nil)
489
+ return initialResult
333
490
  }
334
491
  let observedText = editableTextValue(for: repairTarget) ?? ""
335
492
  NSLog(
@@ -338,10 +495,15 @@ extension RunnerTests {
338
495
  observedText.count
339
496
  )
340
497
  clearTextInput(repairTarget)
341
- app.typeText(expectedText)
342
- return verifyTextEntry(app: app, target: target, expectedText: expectedText, repaired: true)
498
+ repairTarget.typeText(expectedText)
499
+ return verifyTextEntry(
500
+ app: app,
501
+ target: target.withElement(repairTarget),
502
+ expectedText: expectedText,
503
+ repaired: true
504
+ )
343
505
  #else
344
- return TextEntryResult(verified: nil, repaired: false, expectedText: expectedText, observedText: nil)
506
+ return initialResult
345
507
  #endif
346
508
  }
347
509
 
@@ -116,6 +116,12 @@ extension RunnerTests {
116
116
  completion((jsonResponse(status: 200, response: executeUptime()), false))
117
117
  return
118
118
  }
119
+ // Re-sends of a still-executing commandId (the daemon's transport retry loop) attach to
120
+ // the in-flight execution and receive its response instead of piling a second execution
121
+ // onto the main queue behind it (#1105 capture pileup).
122
+ if attachToInFlightCommandIfNeeded(command: command, completion: completion) {
123
+ return
124
+ }
119
125
  NSLog(
120
126
  "AGENT_DEVICE_RUNNER_COMMAND_ACCEPTED command=%@ commandId=%@",
121
127
  command.command.rawValue,
@@ -131,7 +137,11 @@ extension RunnerTests {
131
137
  command.commandId ?? "",
132
138
  response.ok ? 1 : 0
133
139
  )
134
- completion((self.jsonResponse(status: 200, response: response), command.command == .shutdown))
140
+ self.deliverCommandResult(
141
+ command: command,
142
+ result: (self.jsonResponse(status: 200, response: response), command.command == .shutdown),
143
+ completion: completion
144
+ )
135
145
  } catch {
136
146
  NSLog(
137
147
  "AGENT_DEVICE_RUNNER_COMMAND_FAILED command=%@ commandId=%@ error=%@",
@@ -139,17 +149,21 @@ extension RunnerTests {
139
149
  command.commandId ?? "",
140
150
  String(describing: error)
141
151
  )
142
- completion((
143
- self.jsonResponse(
144
- status: 500,
145
- response: self.errorResponse(
146
- code: "COMMAND_FAILED",
147
- message: error.localizedDescription,
148
- hint: "Check the runner log for XCTest details, then retry after the app is foregrounded if this was a timeout or activation failure."
149
- )
152
+ self.deliverCommandResult(
153
+ command: command,
154
+ result: (
155
+ self.jsonResponse(
156
+ status: 500,
157
+ response: self.errorResponse(
158
+ code: "COMMAND_FAILED",
159
+ message: error.localizedDescription,
160
+ hint: "Check the runner log for XCTest details, then retry after the app is foregrounded if this was a timeout or activation failure."
161
+ )
162
+ ),
163
+ false
150
164
  ),
151
- false
152
- ))
165
+ completion: completion
166
+ )
153
167
  }
154
168
  }
155
169
  } catch {
@@ -167,6 +181,60 @@ extension RunnerTests {
167
181
  }
168
182
  }
169
183
 
184
+ // MARK: - In-Flight Command Coalescing
185
+
186
+ /// Returns true when this send duplicated a still-executing commandId and was attached as a
187
+ /// waiter of the in-flight execution. Otherwise marks the commandId in flight and returns
188
+ /// false so the caller enqueues the (single) execution.
189
+ private func attachToInFlightCommandIfNeeded(
190
+ command: Command,
191
+ completion: @escaping ((data: Data, shouldFinish: Bool)) -> Void
192
+ ) -> Bool {
193
+ guard let commandId = normalizedInFlightCommandId(command.commandId) else { return false }
194
+ inFlightCommandLock.lock()
195
+ if inFlightCommandIds.contains(commandId) {
196
+ inFlightCommandWaiters[commandId, default: []].append(completion)
197
+ inFlightCommandLock.unlock()
198
+ NSLog(
199
+ "AGENT_DEVICE_RUNNER_COMMAND_COALESCED command=%@ commandId=%@",
200
+ command.command.rawValue,
201
+ commandId
202
+ )
203
+ return true
204
+ }
205
+ inFlightCommandIds.insert(commandId)
206
+ inFlightCommandLock.unlock()
207
+ return false
208
+ }
209
+
210
+ private func deliverCommandResult(
211
+ command: Command,
212
+ result: (data: Data, shouldFinish: Bool),
213
+ completion: ((data: Data, shouldFinish: Bool)) -> Void
214
+ ) {
215
+ var waiters: [((data: Data, shouldFinish: Bool)) -> Void] = []
216
+ if let commandId = normalizedInFlightCommandId(command.commandId) {
217
+ inFlightCommandLock.lock()
218
+ inFlightCommandIds.remove(commandId)
219
+ waiters = inFlightCommandWaiters.removeValue(forKey: commandId) ?? []
220
+ inFlightCommandLock.unlock()
221
+ }
222
+ completion(result)
223
+ for waiter in waiters {
224
+ waiter(result)
225
+ }
226
+ }
227
+
228
+ private func normalizedInFlightCommandId(_ commandId: String?) -> String? {
229
+ guard let trimmed = commandId?.trimmingCharacters(in: .whitespacesAndNewlines),
230
+ !trimmed.isEmpty
231
+ else {
232
+ return nil
233
+ }
234
+ return trimmed
235
+ }
236
+
237
+
170
238
  // MARK: - Response Encoding
171
239
 
172
240
  private func jsonResponse(status: Int, response: Response) -> Data {
@@ -203,6 +271,9 @@ extension RunnerTests {
203
271
  private func finish() {
204
272
  listener?.cancel()
205
273
  listener = nil
274
+ // Guard against double-fulfill: coalesced shutdown sends deliver one result to
275
+ // multiple waiters, each of which may ask to finish.
206
276
  doneExpectation?.fulfill()
277
+ doneExpectation = nil
207
278
  }
208
279
  }
@@ -54,6 +54,41 @@ final class RunnerTests: XCTestCase {
54
54
  var needsFirstInteractionDelay = false
55
55
  var activeRecording: ScreenRecorder?
56
56
  let commandJournal = RunnerCommandJournal()
57
+ // Coalesces duplicate transport sends of the same commandId onto the single in-flight
58
+ // execution instead of enqueueing them again behind it (#1105 capture pileup).
59
+ let inFlightCommandLock = NSLock()
60
+ var inFlightCommandIds: Set<String> = []
61
+ var inFlightCommandWaiters: [String: [((data: Data, shouldFinish: Bool)) -> Void]] = [:]
62
+ // Tracks main-queue work abandoned by the execution watchdog so new main-thread commands
63
+ // fail fast as busy instead of queueing behind work that cannot be cancelled (#1105).
64
+ let mainThreadWorkLock = NSLock()
65
+ var abandonedMainThreadWorkCount = 0
66
+ var abandonedMainThreadWorkSince: Date?
67
+ // Past this age the runner stops claiming "busy, retry soon" and reports itself wedged so
68
+ // the daemon recycles it — the only cure once the main thread is stuck for good.
69
+ let mainThreadWedgeThreshold: TimeInterval = 120
70
+ // Sticky per-bundle hint: after the XCTest tree backend ground past its slice (or a snapshot
71
+ // was abandoned by the watchdog), later capture plans lead with the private AX backend
72
+ // instead of re-grinding the tree on the same screen class (#1105).
73
+ let snapshotTreePenaltyLock = NSLock()
74
+ var snapshotTreePenaltyBundleId: String?
75
+ var snapshotTreePenaltyUntil = Date.distantPast
76
+ let snapshotTreePenaltyDuration: TimeInterval = 120
77
+ // Bluesky-class screens grind ~4-8s before the tree backend fails; anything past this
78
+ // threshold marks the screen hostile so the next capture leads with private AX.
79
+ let snapshotTreeSlowCaptureThreshold: TimeInterval = 3
80
+ // The blocking XCTest tree snapshot XPC runs on a worker thread with this slice so a
81
+ // content-dependent grind (#1105: seconds to minutes on live Bluesky screens) cannot pin
82
+ // the capture plan. On timeout the XPC keeps grinding on its worker; while any abandoned
83
+ // tree capture is outstanding, plans skip the XCTest-backed tiers (tree, query sweep) and
84
+ // use the private AX backend, which does not go through testmanagerd.
85
+ let treeCaptureLock = NSLock()
86
+ var abandonedTreeCaptureCount = 0
87
+ let treeCaptureSliceBudget: TimeInterval = 8
88
+ // Observability for the record(_:) suppression below: how many AX-broken-screen snapshot
89
+ // issues this session muted, so wedge investigations see the volume without grepping logs.
90
+ let suppressedIssueLock = NSLock()
91
+ var suppressedAxSnapshotIssueCount = 0
57
92
  let interactiveTypes: Set<XCUIElement.ElementType> = [
58
93
  .button,
59
94
  .cell,
@@ -88,6 +123,43 @@ final class RunnerTests: XCTestCase {
88
123
  continueAfterFailure = true
89
124
  }
90
125
 
126
+ /// True for the one recorded-issue class the runner deliberately mutes: an AX-server error
127
+ /// (`kAXError*`) inside a "Failed to get matching snapshot" fetch. The kAXError token
128
+ /// intentionally covers kAXErrorIllegalArgument and its sibling AX server codes (e.g.
129
+ /// kAXErrorCannotComplete): any AX-server rejection inside a matching-snapshot fetch is the
130
+ /// same capture-plan noise the plan already classifies and recovers from. The timeout
131
+ /// variant ("Failed to get matching snapshot: Timed out while evaluating UI query.") carries
132
+ /// no kAXError token and MUST keep recording — it signals a genuinely hung query, exactly
133
+ /// the pathology XCTEST_RECORDED_FAILURE must stay able to see.
134
+ static func isSuppressedAxSnapshotIssueDescription(_ description: String) -> Bool {
135
+ description.contains("Failed to get matching snapshot") && description.contains("kAXError")
136
+ }
137
+
138
+ /// On AX-broken screens (deep RN trees, #758/#1105) XCUIApplication queries record
139
+ /// "Failed to get matching snapshot: ... kAXError..." issues; XCTest tears the whole test
140
+ /// case down once a few accumulate, killing the long-lived runner right after the command
141
+ /// completes and forcing a ~25s restart per capture. This override is deliberately
142
+ /// suite-global (all commands, not just snapshot capture): tap-triggered element queries on
143
+ /// the same screens record the same noise and would still tear the runner down, and command
144
+ /// outcomes stay honest through their own error paths — only this issue side-channel is
145
+ /// muted. Everything else still records (and still drives XCTEST_RECORDED_FAILURE).
146
+ override func record(_ issue: XCTIssue) {
147
+ let description = issue.compactDescription
148
+ if Self.isSuppressedAxSnapshotIssueDescription(description) {
149
+ suppressedIssueLock.lock()
150
+ suppressedAxSnapshotIssueCount += 1
151
+ let count = suppressedAxSnapshotIssueCount
152
+ suppressedIssueLock.unlock()
153
+ NSLog(
154
+ "AGENT_DEVICE_RUNNER_AX_SNAPSHOT_ISSUE_SUPPRESSED count=%ld description=%@",
155
+ count,
156
+ description
157
+ )
158
+ return
159
+ }
160
+ super.record(issue)
161
+ }
162
+
91
163
  @MainActor
92
164
  func testCommand() throws {
93
165
  if RunnerEnv.isTruthy("AGENT_DEVICE_RUNNER_NOOP_STARTUP") {
@@ -0,0 +1 @@
1
+ import{t as e}from"./rolldown-runtime.js";import{F as t,I as n}from"./sdk-contracts.js";import{T as r,j as i,v as a}from"./sdk-android-adb.js";import{t as o}from"./timeouts.js";import{c as s,i as c,n as l,o as u,t as d}from"./host-process.js";import f from"node:crypto";import p from"node:fs";import m from"node:os";import h from"node:path";async function g(e){let{lockDirPath:n,owner:r}=e,i=h.join(n,`owner.json`),a=Date.now()+(e.timeoutMs??3e4),s=e.pollMs??100,c=e.ownerGraceMs??5e3,l=e.description??`process lock`;for(p.mkdirSync(h.dirname(n),{recursive:!0});Date.now()<a;)try{p.mkdirSync(n),_(i,r);let e=!1;return async()=>{e||(e=!0,p.rmSync(n,{recursive:!0,force:!0}))}}catch(e){let t=e;if(t.code!==`EEXIST`)throw t;if(ee(n,i,c))continue;await o(s)}throw new t(`COMMAND_FAILED`,`Timed out waiting for ${l}`,{lockDirPath:n,...y(n,i)})}function _(e,t){let n=`${e}.${process.pid}.${Date.now()}.tmp`;p.writeFileSync(n,JSON.stringify(t),`utf8`),p.renameSync(n,e)}function ee(e,t,n){let r=null;try{r=p.statSync(e)}catch{return!0}let i=v(t);return i?b(i)?!1:(p.rmSync(e,{recursive:!0,force:!0}),!0):Date.now()-r.mtimeMs<n?!1:(p.rmSync(e,{recursive:!0,force:!0}),!0)}function v(e){try{return JSON.parse(p.readFileSync(e,`utf8`))}catch{return null}}function y(e,t){let n=Date.now(),r=v(t),i;try{i=Math.max(0,Math.round(n-p.statSync(e).mtimeMs))}catch{}return{...i===void 0?{}:{lockAgeMs:i},...r?{ownerPid:r.pid,ownerStartTime:r.startTime,ownerAgeMs:Math.max(0,Math.round(n-r.acquiredAtMs))}:{}}}function b(e){return!Number.isInteger(e.pid)||e.pid<=0||!l(e.pid)?!1:e.startTime?u(e.pid)===e.startTime:!0}const x=new Map;function S(e){let t=f.createHash(`sha256`);return t.update(h.resolve(e.stateDir)),t.update(`\0`),t.update(h.resolve(e.installDir)),`--agent-device-managed-web=${t.digest(`hex`).slice(0,16)}`}function C(e,t){let n=S(t),r=e?.trim();return r?k(r).includes(n)?r:`${r},${n}`:n}function w(e){return A(e.AGENT_BROWSER_IDLE_TIMEOUT_MS)??A(e.AGENT_DEVICE_WEB_IDLE_TIMEOUT_MS)??3e5}function te(e,t){if(e.pid!==process.pid&&j(e.command)){if(e.command.includes(S(t)))return{process:e,reason:`launch-marker`};if(M(t).some(t=>re(e,t)))return{process:e,reason:`managed-browser-home`}}}function T(e,t){let n=e.flatMap(e=>{let n=te(e,t);return n?[n]:[]});return{count:n.length,pids:n.map(e=>e.process.pid),processes:n}}async function E(e){return T(await O(),e)}async function D(e,t,n={}){let r=N([...n.openWebSessionNames??[]]);if(r.length>0)return P(`open-web-session`,{openWebSessionNames:r});let a=w(process.env),o=I(e);if(o!==void 0&&Date.now()-o<a)return P(`recent-browser-activity`,{idleTimeoutMs:a,latestActivityMs:o});let c=await O(),l=T(c,e),u=d(l.processes.map(e=>e.process.pid),c).map(e=>e.pid),f={...l,signalPids:u};return l.count===0?f:(i({level:`warn`,phase:`web_agent_browser_orphan_cleanup`,data:{reason:t,count:l.count,pids:l.pids,signalPids:u,stateDir:e.stateDir,installDir:e.installDir,matchReasons:l.processes.map(e=>e.reason)}}),await s({pids:u,termTimeoutMs:1500,killTimeoutMs:1e3}),f)}async function ne(e,t={}){let n=Date.now(),r=h.resolve(e.stateDir),i=x.get(r);if(!(i!==void 0&&n-i<3e4))return x.set(r,n),await D(e,`provider-startup`,t)}async function O(){return await r(async()=>await c({timeoutMs:1500}))}function k(e){return e.split(/[,\n]/).map(e=>e.trim()).filter(Boolean)}function A(e){if(e===void 0||e.trim()===``)return;let t=Number.parseInt(e,10);return Number.isInteger(t)&&t>0?t:void 0}function j(e){return/\b(?:Google Chrome for Testing|Chrome for Testing|Chromium|chrome|chrome\.exe|chromium|chromium-browser|headless_shell)\b/i.test(e)}function M(e){return N([h.join(e.homeDir,`.agent-browser`,`browsers`),h.join(e.runtimeHomeDir,`.agent-browser`,`browsers`)])}function N(e){return[...new Set(e.filter(e=>e.length>0))]}function P(e,t){return{count:0,pids:[],processes:[],signalPids:[],skipped:{reason:e,...t}}}function re(e,t){let n=F(e.command),r=`${F(h.resolve(t)).replace(/\/+$/,``)}/`;return n.includes(r)}function F(e){return e.replace(/\\/g,`/`)}function I(e){let t=L(e.socketDir).map(e=>R(e)).filter(e=>e!==void 0);return t.length>0?Math.max(...t):void 0}function L(e){try{return p.readdirSync(e).map(t=>h.join(e,t))}catch{return[]}}function R(e){try{return p.statSync(e).mtimeMs}catch{return}}var z=e({doctorManagedAgentBrowser:()=>K,getManagedAgentBrowserStatus:()=>q,mapManagedAgentBrowserError:()=>$,resolveAgentBrowserTool:()=>W,setupManagedAgentBrowser:()=>G});const B=`0.27.1`,V=`agent-browser`,H=5*6e4,U=6e4;async function W(e){let t=q(e);if(t.installed)return J(t);throw Q(t)}async function G(e){let t=q(e);ce(t.nodeMajor);let n=await g({lockDirPath:h.join(t.installDir,`..`,`.agent-browser-install.lock`),owner:{pid:process.pid,startTime:u(process.pid),acquiredAtMs:Date.now()},timeoutMs:H,description:`managed agent-browser setup`});try{let t=q(e);return t.installed?t:(p.mkdirSync(t.installDir,{recursive:!0}),await Y(t),await X(t,[`install`],{timeoutMs:H}),await X(t,[`doctor`,`--offline`,`--quick`],{timeoutMs:U}),ae(t),q(e))}finally{await n()}}async function K(e){let t=q(e);if(!t.installed)throw Q(t);return{status:t,...await X(t,[`doctor`,`--offline`,`--quick`],{timeoutMs:U,allowFailure:!0})}}function q(e){let t=e.stateDir??process.env.AGENT_DEVICE_STATE_DIR??pe(),n=h.join(t,`tools`,`agent-browser`,B),r=se(n),i=h.join(n,`home`),a=le(n),o=ue(n),s=fe(r)&&oe(n),c=Number.parseInt(process.versions.node.split(`.`)[0]??`0`,10);return{version:B,stateDir:t,installDir:n,binaryPath:r,homeDir:i,runtimeHomeDir:a,socketDir:o,installed:s,nodeMajor:c,nodeSupported:c>=24}}function J(e){if(!e.installed)throw Q(e);return{command:e.binaryPath,env:Z(e,process.env)}}async function Y(e){let t=h.join(e.installDir,`package`);p.mkdirSync(t,{recursive:!0}),await a(`npm`,[`install`,`--prefix`,t,`--no-audit`,`--no-fund`,`--no-save`,`${V}@${B}`],{env:process.env,timeoutMs:H})}async function X(e,t,n){return await a(e.binaryPath,t,{allowFailure:n.allowFailure,env:Z(e,process.env),timeoutMs:n.timeoutMs})}function Z(e,t){return p.mkdirSync(e.homeDir,{recursive:!0}),ie(e),p.mkdirSync(e.socketDir,{recursive:!0}),{...t,HOME:e.runtimeHomeDir,AGENT_BROWSER_SOCKET_DIR:e.socketDir,AGENT_BROWSER_IDLE_TIMEOUT_MS:String(w(t)),AGENT_BROWSER_ARGS:C(t.AGENT_BROWSER_ARGS,e)}}function ie(e){if(e.runtimeHomeDir!==e.homeDir){p.mkdirSync(h.dirname(e.runtimeHomeDir),{recursive:!0});try{let t=p.lstatSync(e.runtimeHomeDir);if(t.isSymbolicLink()&&p.readlinkSync(e.runtimeHomeDir)===e.homeDir||t.isDirectory())return;t.isSymbolicLink()&&p.unlinkSync(e.runtimeHomeDir)}catch(e){if(!de(e))throw e}try{p.symlinkSync(e.homeDir,e.runtimeHomeDir,`dir`)}catch{p.mkdirSync(e.runtimeHomeDir,{recursive:!0})}}}function ae(e){p.writeFileSync(h.join(e.installDir,`manifest.json`),JSON.stringify({package:V,version:B,node:process.version,installedAt:new Date().toISOString()},null,2),`utf8`)}function oe(e){return p.existsSync(h.join(e,`manifest.json`))}function se(e){let t=h.join(e,`package`);return h.join(t,`node_modules`,`.bin`,process.platform===`win32`?`agent-browser.cmd`:`agent-browser`)}function Q(e){return new t(`TOOL_MISSING`,`Managed web browser backend is not installed.`,{version:B,installDir:e.installDir,hint:e.nodeSupported===!1?`Web automation requires Node 24+; current Node is ${process.version}.`:"Run `agent-device web setup` to install the managed web backend."})}function ce(e){if(!(e>=24))throw new t(`UNSUPPORTED_OPERATION`,`Web automation requires Node 24 or newer.`,{currentNode:process.version,requiredNodeMajor:24,hint:`Run agent-device with Node 24+ for web setup and web automation.`})}function le(e){if(process.platform===`win32`)return h.join(e,`home`);let t=f.createHash(`sha1`).update(e).digest(`hex`).slice(0,12);return h.join(m.tmpdir(),`agent-device-web`,t)}function ue(e){let t=f.createHash(`sha1`).update(e).digest(`hex`).slice(0,12);return h.join(m.tmpdir(),`adw`,t)}function de(e){return typeof e==`object`&&!!e&&`code`in e&&e.code===`ENOENT`}function fe(e){try{return p.accessSync(e,p.constants.X_OK),!0}catch{return!1}}function pe(){return h.join(process.env.HOME??process.cwd(),`.agent-device`)}function $(e){let r=n(e);return r.code===`TOOL_MISSING`?new t(r.code,r.message,{...r.details??{},hint:typeof r.details?.hint==`string`?r.details.hint:"Run `agent-device web setup` to install the managed web backend."}):r}export{D as a,g as c,W as i,q as n,ne as o,$ as r,E as s,z as t};