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.
- 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
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.0.apk.sha256 +1 -0
- 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
- 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
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.0.apk.sha256 +1 -0
- 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
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTapPointPolicy.swift +28 -0
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+AXSnapshotFallback.swift +9 -120
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +132 -66
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +155 -0
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +67 -0
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +20 -74
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +64 -0
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +0 -177
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +81 -48
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +102 -129
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +208 -46
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +82 -11
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +72 -0
- package/dist/src/agent-browser-tool.js +1 -0
- package/dist/src/android-adb.d.ts +64 -234
- package/dist/src/android-adb.js +1 -1
- package/dist/src/android.js +1 -5
- package/dist/src/app-lifecycle.js +7 -0
- package/dist/src/apple-runner-options.js +1 -0
- package/dist/src/apps.js +5 -11
- package/dist/src/artifacts.d.ts +4 -3
- package/dist/src/artifacts.js +1 -1
- package/dist/src/audio-probe.js +1 -1
- package/dist/src/batch.d.ts +2 -177
- package/dist/src/batch.js +1 -1
- package/dist/src/cli-help.js +58 -37
- package/dist/src/cli-output.js +1 -1
- package/dist/src/cli-runner.js +2 -0
- package/dist/src/cli.js +12 -41
- package/dist/src/client-metro-companion.js +1 -0
- package/dist/src/client.js +3 -0
- package/dist/src/command-metadata.js +1 -0
- package/dist/src/command-schema.js +22 -0
- package/dist/src/command-surface.js +1 -1
- package/dist/src/contracts.d.ts +2 -213
- package/dist/src/contracts.js +1 -1
- package/dist/src/debug-symbols.js +5 -3
- package/dist/src/device-input-state.js +2 -0
- package/dist/src/device-rotation.js +14 -0
- package/dist/src/devices.js +1 -1
- package/dist/src/devices2.js +1 -0
- package/dist/src/devices3.js +1 -0
- package/dist/src/find.js +1 -1
- package/dist/src/finders.d.ts +2 -81
- package/dist/src/finders.js +1 -1
- package/dist/src/generic.js +1 -1
- package/dist/src/handler-utils.js +1 -1
- package/dist/src/helper.js +2 -0
- package/dist/src/host-process.js +2 -0
- package/dist/src/index.d.ts +1265 -1786
- package/dist/src/index.js +1 -1
- package/dist/src/input-actions.js +3 -1
- package/dist/src/input-actions2.js +1 -0
- package/dist/src/install-source.d.ts +13 -14
- package/dist/src/install-source.js +1 -1
- package/dist/src/install-source2.js +1 -0
- package/dist/src/interaction-snapshot.js +1 -0
- package/dist/src/interaction.js +1 -1
- package/dist/src/interactor.js +2 -1
- package/dist/src/internal/bin.d.ts +1 -1
- package/dist/src/internal/bin.js +2 -5
- package/dist/src/internal/companion-tunnel.d.ts +1 -1
- package/dist/src/internal/companion-tunnel.js +1 -1
- package/dist/src/internal/daemon.d.ts +1 -1
- package/dist/src/internal/daemon.js +16 -1
- package/dist/src/internal/png-worker.d.ts +29 -26
- package/dist/src/internal/png-worker.js +1 -1
- package/dist/src/internal/update-check-entry.d.ts +1 -1
- package/dist/src/internal/update-check-entry.js +1 -1
- package/dist/src/io.d.ts +2 -85
- package/dist/src/io.js +1 -1
- package/dist/src/lease.js +1 -1
- package/dist/src/linux.js +1 -1
- package/dist/src/logcat.js +6 -0
- package/dist/src/manifest.js +1 -0
- package/dist/src/metro.d.ts +70 -113
- package/dist/src/metro.js +1 -1
- package/dist/src/metro2.js +1 -0
- package/dist/src/notifications.js +1 -1
- package/dist/src/output-path.js +1 -0
- package/dist/src/path-resolution.js +1 -0
- package/dist/src/png-worker-contract.js +1 -0
- package/dist/src/points.js +4 -0
- package/dist/src/project-runtime.js +1 -0
- package/dist/src/provider.js +1 -1
- package/dist/src/providers.js +1 -0
- package/dist/src/react-native.js +1 -1
- package/dist/src/record-trace-recording.js +2 -5
- package/dist/src/record-trace.js +1 -1
- package/dist/src/recording-provider.js +1 -1
- package/dist/src/registry.js +37 -0
- package/dist/src/remote-config.d.ts +2 -83
- package/dist/src/reporting.js +9 -0
- package/dist/src/rolldown-runtime.js +1 -0
- package/dist/src/runner-client.js +1 -0
- package/dist/src/runner-provider.js +1 -0
- package/dist/src/runner-sequence.js +1 -1
- package/dist/src/runtime-transport.js +1 -0
- package/dist/src/runtime.js +3 -0
- package/dist/src/scoped-provider.js +1 -0
- package/dist/src/sdk-android-adb.d.ts +144 -0
- package/dist/src/sdk-android-adb.js +4 -0
- package/dist/src/sdk-app-inventory.d.ts +4 -0
- package/dist/src/sdk-app-inventory.js +1 -0
- package/dist/src/sdk-batch-runner.d.ts +40 -0
- package/dist/src/sdk-batch-runner.js +2 -0
- package/dist/src/sdk-batch.js +2 -0
- package/dist/src/sdk-contracts.d.ts +386 -0
- package/dist/src/sdk-contracts.js +2 -0
- package/dist/src/sdk-finders.d.ts +61 -0
- package/dist/src/sdk-finders.js +1 -0
- package/dist/src/sdk-io.d.ts +79 -0
- package/dist/src/sdk-io.js +1 -0
- package/dist/src/sdk-remote-config.d.ts +140 -0
- package/dist/src/sdk-remote-config.js +1 -0
- package/dist/src/sdk-selectors.js +1 -0
- package/dist/src/selector-runtime.js +1 -1
- package/dist/src/selectors.d.ts +39 -137
- package/dist/src/selectors.js +1 -1
- package/dist/src/selectors2.js +1 -0
- package/dist/src/server.js +3 -2
- package/dist/src/session.js +10 -11
- package/dist/src/setting-state.js +1 -0
- package/dist/src/simctl.js +1 -0
- package/dist/src/simulator.js +1 -2
- package/dist/src/snapshot-diagnostics.js +3 -0
- package/dist/src/snapshot-helper.js +5 -0
- package/dist/src/snapshot-quality.js +1 -0
- package/dist/src/snapshot.js +1 -2
- package/dist/src/snapshot2.js +2 -0
- package/dist/src/{5560.js → timeouts.js} +1 -1
- package/dist/src/tool-provider.js +3 -0
- package/dist/src/tool-provider2.js +1 -0
- package/dist/src/ttl-memo.js +1 -0
- package/dist/src/update-check.js +1 -0
- package/dist/src/url.js +1 -0
- package/dist/src/version.js +1 -0
- package/dist/src/web.js +1 -1
- package/dist/src/xml.js +2 -0
- package/package.json +12 -19
- package/server.json +2 -2
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.18.3.apk.sha256 +0 -1
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.18.3.apk.sha256 +0 -1
- package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +0 -443
- package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+FlatSnapshotFiltering.swift +0 -169
- package/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +0 -220
- package/dist/src/1404.js +0 -1
- package/dist/src/1986.js +0 -1
- package/dist/src/2748.js +0 -1
- package/dist/src/2948.js +0 -30
- package/dist/src/3006.js +0 -1
- package/dist/src/314.js +0 -1
- package/dist/src/3340.js +0 -8
- package/dist/src/4418.js +0 -1
- package/dist/src/4428.js +0 -3
- package/dist/src/485.js +0 -1
- package/dist/src/4917.js +0 -1
- package/dist/src/495.js +0 -5
- package/dist/src/5448.js +0 -3
- package/dist/src/5484.js +0 -1
- package/dist/src/5643.js +0 -1
- package/dist/src/587.js +0 -1
- package/dist/src/5913.js +0 -4
- package/dist/src/6849.js +0 -1
- package/dist/src/6918.js +0 -1
- package/dist/src/6967.js +0 -1
- package/dist/src/760.js +0 -1
- package/dist/src/7871.js +0 -1
- package/dist/src/8184.js +0 -1
- package/dist/src/8225.js +0 -1
- package/dist/src/8391.js +0 -1
- package/dist/src/8404.js +0 -2
- package/dist/src/8414.js +0 -2
- package/dist/src/8875.js +0 -4
- package/dist/src/893.js +0 -1
- package/dist/src/8936.js +0 -1
- package/dist/src/9414.js +0 -1
- package/dist/src/9581.js +0 -22
- package/dist/src/9612.js +0 -1
- package/dist/src/9624.js +0 -1
- package/dist/src/9722.js +0 -39
- package/dist/src/devices~1.js +0 -1
- package/dist/src/devices~2.js +0 -1
- package/dist/src/input-actions~1.js +0 -1
- package/dist/src/rslib-runtime.js +0 -1
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner/AgentDeviceRunnerApp.m +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/xcshareddata/xcschemes/AgentDeviceRunner.xcscheme +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests-Bridging-Header.h +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/AgentDeviceRunnerUITests.entitlements +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.h +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerAXSnapshotBridge.m +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.h +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerObjCExceptionCatcher.m +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.h +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedGesture.m +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Alert.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Environment.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Exceptions.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Lifecycle.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScreenRecorder.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TvRemote.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests.xctestplan +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-overlay.swift +0 -0
- /package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/RecordingScripts/recording-resize.swift +0 -0
- /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(
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
|
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
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
342
|
-
return verifyTextEntry(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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};
|