agent-device 0.18.3 → 0.19.1
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/README.md +5 -1
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.18.3.apk → agent-device-android-multitouch-helper-0.19.1.apk} +0 -0
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.19.1.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.1.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.1.apk} +0 -0
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.19.1.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.1.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 +33 -127
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +630 -177
- 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 +173 -122
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift +172 -0
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +64 -0
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +76 -182
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +92 -51
- package/{apple-runner → dist/apple-runner}/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SnapshotCapturePlan.swift +213 -137
- package/dist/apple-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +142 -0
- 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 +74 -0
- package/dist/src/agent-browser-tool.js +1 -0
- package/dist/src/android-adb.d.ts +63 -234
- package/dist/src/android-adb.js +1 -1
- package/dist/src/android-input-ownership.js +6 -0
- 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 +178 -60
- 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/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 +1444 -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 +18 -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/manifest.js +1 -0
- package/dist/src/metro.d.ts +69 -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 +38 -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 +39 -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 +60 -0
- package/dist/src/sdk-finders.js +1 -0
- package/dist/src/sdk-io.d.ts +78 -0
- package/dist/src/sdk-io.js +1 -0
- package/dist/src/sdk-remote-config.d.ts +139 -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 +38 -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/tv-remote.js +14 -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 +13 -19
- package/server.json +2 -2
- package/skills/agent-device/SKILL.md +8 -7
- 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
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
|
|
3
|
+
// Runner-local policy for AX-free/private synthesized iOS gestures.
|
|
4
|
+
//
|
|
5
|
+
// This policy is intentionally separate from the TS interaction guarantee matrix:
|
|
6
|
+
// ADR 0011 models element-targeting guarantees, while this module models command
|
|
7
|
+
// paths that must keep scroll/drag/sequence usable when XCTest AX is unhealthy.
|
|
8
|
+
|
|
9
|
+
enum RunnerAccessibilityHealth: String, Equatable {
|
|
10
|
+
case unknown
|
|
11
|
+
case healthy
|
|
12
|
+
case unavailable
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
enum SynthesizedKeyboardPolicy: String, Equatable, Hashable {
|
|
16
|
+
case never
|
|
17
|
+
case whenAccessibilityHealthy
|
|
18
|
+
case requiredWhenAvailable
|
|
19
|
+
|
|
20
|
+
func allowsProbe(accessibilityHealth: RunnerAccessibilityHealth) -> Bool {
|
|
21
|
+
switch self {
|
|
22
|
+
case .never:
|
|
23
|
+
return false
|
|
24
|
+
case .whenAccessibilityHealthy:
|
|
25
|
+
return accessibilityHealth == .healthy
|
|
26
|
+
case .requiredWhenAvailable:
|
|
27
|
+
return accessibilityHealth != .unavailable
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
enum SynthesizedFallbackPolicy: String, Equatable, Hashable {
|
|
33
|
+
case privateSynthesisRequired
|
|
34
|
+
case xctestCoordinateWhenAccessibilityAvailable
|
|
35
|
+
case xctestCoordinateAllowed
|
|
36
|
+
|
|
37
|
+
func allowsXCTestCoordinateFallback(accessibilityHealth: RunnerAccessibilityHealth) -> Bool {
|
|
38
|
+
switch self {
|
|
39
|
+
case .privateSynthesisRequired:
|
|
40
|
+
return false
|
|
41
|
+
case .xctestCoordinateWhenAccessibilityAvailable:
|
|
42
|
+
return accessibilityHealth != .unavailable
|
|
43
|
+
case .xctestCoordinateAllowed:
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
enum SynthesizedGesturePolicyKind: String, Equatable, Hashable {
|
|
50
|
+
case coordinateTap
|
|
51
|
+
case scroll
|
|
52
|
+
case synthesizedDrag
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
struct SynthesizedGesturePolicy: Equatable, Hashable {
|
|
56
|
+
let keyboardPolicy: SynthesizedKeyboardPolicy
|
|
57
|
+
let fallbackPolicy: SynthesizedFallbackPolicy
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
struct SynthesizedCoordinateContext {
|
|
61
|
+
let referenceFrame: CGRect
|
|
62
|
+
let keyboardPolicy: SynthesizedKeyboardPolicy
|
|
63
|
+
let fallbackPolicy: SynthesizedFallbackPolicy
|
|
64
|
+
let accessibilityHealth: RunnerAccessibilityHealth
|
|
65
|
+
|
|
66
|
+
func withReferenceFrame(_ frame: CGRect) -> SynthesizedCoordinateContext {
|
|
67
|
+
SynthesizedCoordinateContext(
|
|
68
|
+
referenceFrame: frame,
|
|
69
|
+
keyboardPolicy: keyboardPolicy,
|
|
70
|
+
fallbackPolicy: fallbackPolicy,
|
|
71
|
+
accessibilityHealth: accessibilityHealth
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
var allowsXCTestCoordinateFallback: Bool {
|
|
76
|
+
fallbackPolicy.allowsXCTestCoordinateFallback(accessibilityHealth: accessibilityHealth)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
var allowsKeyboardProbe: Bool {
|
|
80
|
+
keyboardPolicy.allowsProbe(accessibilityHealth: accessibilityHealth)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func synthesizedGesturePolicy(_ kind: SynthesizedGesturePolicyKind) -> SynthesizedGesturePolicy {
|
|
85
|
+
switch kind {
|
|
86
|
+
case .coordinateTap:
|
|
87
|
+
return SynthesizedGesturePolicy(
|
|
88
|
+
keyboardPolicy: .never,
|
|
89
|
+
fallbackPolicy: .xctestCoordinateAllowed
|
|
90
|
+
)
|
|
91
|
+
case .scroll:
|
|
92
|
+
return SynthesizedGesturePolicy(
|
|
93
|
+
keyboardPolicy: .whenAccessibilityHealthy,
|
|
94
|
+
fallbackPolicy: .privateSynthesisRequired
|
|
95
|
+
)
|
|
96
|
+
case .synthesizedDrag:
|
|
97
|
+
return SynthesizedGesturePolicy(
|
|
98
|
+
keyboardPolicy: .requiredWhenAvailable,
|
|
99
|
+
fallbackPolicy: .xctestCoordinateWhenAccessibilityAvailable
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
func sequenceHasSynthesizedCoordinateStep(_ steps: [SequenceStep]) -> Bool {
|
|
105
|
+
steps.contains { step in
|
|
106
|
+
step.synthesized == true && (step.kind == "tap" || step.kind == "drag")
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
extension RunnerTests {
|
|
111
|
+
func synthesizedSequenceCoordinateContext(steps: [SequenceStep]) -> SynthesizedCoordinateContext? {
|
|
112
|
+
guard sequenceHasSynthesizedCoordinateStep(steps) else { return nil }
|
|
113
|
+
return synthesizedCoordinateContext(policy: synthesizedGesturePolicy(.synthesizedDrag))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
func logSynthesizedGesturePolicyDecision(
|
|
117
|
+
kind: SynthesizedGesturePolicyKind,
|
|
118
|
+
context: SynthesizedCoordinateContext?,
|
|
119
|
+
fallbackAttempted: Bool
|
|
120
|
+
) {
|
|
121
|
+
#if os(iOS)
|
|
122
|
+
guard let context else {
|
|
123
|
+
NSLog(
|
|
124
|
+
"AGENT_DEVICE_RUNNER_SYNTHESIZED_GESTURE_POLICY kind=%@ context=unavailable fallbackAttempted=%@",
|
|
125
|
+
kind.rawValue,
|
|
126
|
+
fallbackAttempted.description
|
|
127
|
+
)
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
NSLog(
|
|
131
|
+
"AGENT_DEVICE_RUNNER_SYNTHESIZED_GESTURE_POLICY kind=%@ axHealth=%@ frameSource=screenshot keyboardPolicy=%@ fallbackPolicy=%@ fallbackAllowed=%@ fallbackAttempted=%@",
|
|
132
|
+
kind.rawValue,
|
|
133
|
+
context.accessibilityHealth.rawValue,
|
|
134
|
+
context.keyboardPolicy.rawValue,
|
|
135
|
+
context.fallbackPolicy.rawValue,
|
|
136
|
+
context.allowsXCTestCoordinateFallback.description,
|
|
137
|
+
fallbackAttempted.description
|
|
138
|
+
)
|
|
139
|
+
#endif
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
@@ -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
|
}
|