agent-device 0.20.3 → 0.20.5

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 (140) hide show
  1. package/android/ime-helper/dist/{agent-device-android-ime-helper-0.20.3.apk → agent-device-android-ime-helper-0.20.5.apk} +0 -0
  2. package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.5.apk.sha256 +1 -0
  3. package/android/ime-helper/dist/{agent-device-android-ime-helper-0.20.3.manifest.json → agent-device-android-ime-helper-0.20.5.manifest.json} +4 -4
  4. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.5.apk +0 -0
  5. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.5.apk.sha256 +1 -0
  6. package/android/snapshot-helper/dist/{agent-device-android-snapshot-helper-0.20.3.manifest.json → agent-device-android-snapshot-helper-0.20.5.manifest.json} +6 -6
  7. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +57 -4
  8. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +45 -9
  9. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Keyboard.swift +73 -0
  10. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Navigation.swift +1 -0
  11. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+ScrollGesture.swift +3 -2
  12. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SequenceExecution.swift +1 -1
  13. package/dist/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SynthesizedGesturePolicy.swift +8 -2
  14. package/dist/src/agent-browser-tool.js +1 -1
  15. package/dist/src/agent-device-client.js +2 -2
  16. package/dist/src/android-adb.d.ts +1 -1
  17. package/dist/src/android.js +1 -1
  18. package/dist/src/app-helpers.js +1 -1
  19. package/dist/src/app-inventory.d.ts +4 -0
  20. package/dist/src/app-lifecycle.js +7 -7
  21. package/dist/src/app-resolution.js +1 -1
  22. package/dist/src/apps.js +5 -5
  23. package/dist/src/boot-diagnostics.js +1 -1
  24. package/dist/src/cli-config.js +1 -1
  25. package/dist/src/cli-help.js +27 -19
  26. package/dist/src/cli-runner.js +1 -1
  27. package/dist/src/cli.js +15 -14
  28. package/dist/src/client-metro.js +1 -1
  29. package/dist/src/{tv-remote.d.ts → cloud-artifacts.d.ts} +109 -126
  30. package/dist/src/command-metadata.js +1 -1
  31. package/dist/src/command-schema.js +1 -1
  32. package/dist/src/command-surface.js +1 -1
  33. package/dist/src/context.js +1 -1
  34. package/dist/src/contracts.d.ts +1 -1
  35. package/dist/src/debug-symbols.js +4 -4
  36. package/dist/src/device-input-state.d.ts +2 -2
  37. package/dist/src/device-input-state.js +1 -1
  38. package/dist/src/device.js +1 -9
  39. package/dist/src/devices2.js +1 -1
  40. package/dist/src/entities.js +1 -0
  41. package/dist/src/find.js +1 -1
  42. package/dist/src/finders.d.ts +1 -1
  43. package/dist/src/finders.js +1 -1
  44. package/dist/src/generic.js +1 -1
  45. package/dist/src/helper.js +2 -2
  46. package/dist/src/ime-lifecycle.js +9 -0
  47. package/dist/src/index.d.ts +592 -555
  48. package/dist/src/input-actions.js +4 -4
  49. package/dist/src/install-artifact.js +4 -4
  50. package/dist/src/install-artifact2.js +1 -1
  51. package/dist/src/install-source2.js +1 -1
  52. package/dist/src/interaction-snapshot.js +1 -1
  53. package/dist/src/interaction.d.ts +7 -4
  54. package/dist/src/interaction.js +1 -1
  55. package/dist/src/interactor.js +2 -2
  56. package/dist/src/interactor2.js +1 -1
  57. package/dist/src/internal/bin.js +1 -1
  58. package/dist/src/internal/daemon.js +14 -14
  59. package/dist/src/internal/png-worker.js +1 -1
  60. package/dist/src/lease-context.js +1 -1
  61. package/dist/src/lease.js +1 -1
  62. package/dist/src/limrun.d.ts +103 -198
  63. package/dist/src/limrun.js +1 -1
  64. package/dist/src/linux.js +1 -1
  65. package/dist/src/logcat.js +1 -1
  66. package/dist/src/manifest.js +1 -1
  67. package/dist/src/metro.d.ts +1 -1
  68. package/dist/src/parser.js +2 -0
  69. package/dist/src/path-resolution.js +1 -1
  70. package/dist/src/physical-device-control.js +1 -1
  71. package/dist/src/plist-xml.js +1 -0
  72. package/dist/src/plist.js +1 -1
  73. package/dist/src/png-worker-contract.js +1 -1
  74. package/dist/src/process-lock.js +1 -1
  75. package/dist/src/provider-limrun-runtime.js +1 -0
  76. package/dist/src/react-native.js +1 -1
  77. package/dist/src/record-trace.js +1 -1
  78. package/dist/src/register-builtins.js +1 -1
  79. package/dist/src/registry.js +22 -22
  80. package/dist/src/remote-config2.js +1 -1
  81. package/dist/src/reporting.js +7 -7
  82. package/dist/src/runner-client.js +1 -1
  83. package/dist/src/runner-disposal.js +1 -1
  84. package/dist/src/runner-lease.js +1 -1
  85. package/dist/src/runner-provider.js +1 -1
  86. package/dist/src/runner-sequence.js +1 -1
  87. package/dist/src/runtime-transport.js +1 -1
  88. package/dist/src/runtime.js +3 -3
  89. package/dist/src/screenshot-geometry.js +1 -0
  90. package/dist/src/screenshot-result.js +6 -0
  91. package/dist/src/sdk-android-adb.d.ts +5 -6
  92. package/dist/src/sdk-android-adb.js +3 -3
  93. package/dist/src/sdk-batch-runner.d.ts +2 -2
  94. package/dist/src/sdk-batch-runner.js +3 -3
  95. package/dist/src/sdk-contracts.d.ts +21 -25
  96. package/dist/src/sdk-contracts.js +2 -2
  97. package/dist/src/sdk-finders.d.ts +1 -1
  98. package/dist/src/sdk-finders.js +1 -1
  99. package/dist/src/sdk-io.js +1 -1
  100. package/dist/src/sdk-remote-config.d.ts +19 -4
  101. package/dist/src/sdk-remote-config.js +1 -1
  102. package/dist/src/sdk-selectors.js +1 -1
  103. package/dist/src/selector-runtime.js +1 -1
  104. package/dist/src/selector-vocabulary.js +1 -1
  105. package/dist/src/selectors.d.ts +1 -1
  106. package/dist/src/selectors.js +1 -1
  107. package/dist/src/server.js +2 -2
  108. package/dist/src/session-target-evidence.js +1 -1
  109. package/dist/src/session.js +9 -8
  110. package/dist/src/setting-state.js +1 -1
  111. package/dist/src/simulator.js +1 -1
  112. package/dist/src/snapshot.js +1 -1
  113. package/dist/src/snapshot2.js +1 -1
  114. package/dist/src/src.js +1 -0
  115. package/dist/src/tool-provider.js +3 -3
  116. package/dist/src/tool-provider2.js +1 -1
  117. package/dist/src/tool-provider3.js +1 -1
  118. package/dist/src/update-check.js +1 -1
  119. package/dist/src/version.js +1 -1
  120. package/dist/src/viewport-dimension.js +14 -0
  121. package/package.json +21 -9
  122. package/server.json +2 -2
  123. package/android/ime-helper/dist/agent-device-android-ime-helper-0.20.3.apk.sha256 +0 -1
  124. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.3.apk +0 -0
  125. package/android/snapshot-helper/dist/agent-device-android-snapshot-helper-0.20.3.apk.sha256 +0 -1
  126. package/dist/src/android-input-method-overlays.js +0 -1
  127. package/dist/src/android-input-ownership.js +0 -1
  128. package/dist/src/apple-multitouch-support.js +0 -1
  129. package/dist/src/click-button.d.ts +0 -5
  130. package/dist/src/compatibility-policy.js +0 -1
  131. package/dist/src/gesture-plan-types.js +0 -1
  132. package/dist/src/gesture-plan.js +0 -1
  133. package/dist/src/launch-console.js +0 -1
  134. package/dist/src/recording-export-quality.js +0 -14
  135. package/dist/src/rolldown-runtime.js +0 -1
  136. package/dist/src/runtime2.js +0 -1
  137. package/dist/src/sdk-app-inventory.d.ts +0 -4
  138. package/dist/src/sdk-app-inventory.js +0 -1
  139. package/dist/src/snapshot-diagnostics.js +0 -6
  140. package/dist/src/xml.js +0 -2
@@ -0,0 +1 @@
1
+ 1f5a95fcf0c6d668a11453644b2e897147932b528c1aa6c74883eaf9d73a5e7f agent-device-android-ime-helper-0.20.5.apk
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "android-ime-helper",
3
- "version": "0.20.3",
4
- "assetName": "agent-device-android-ime-helper-0.20.3.apk",
5
- "sha256": "02777b8c7e55044210eafa255ac92e627977679d75ba4a97f563f3e23dea3267",
3
+ "version": "0.20.5",
4
+ "assetName": "agent-device-android-ime-helper-0.20.5.apk",
5
+ "sha256": "1f5a95fcf0c6d668a11453644b2e897147932b528c1aa6c74883eaf9d73a5e7f",
6
6
  "packageName": "com.callstack.agentdevice.imehelper",
7
- "versionCode": 20003,
7
+ "versionCode": 20005,
8
8
  "serviceComponent": "com.callstack.agentdevice.imehelper/.TestInputMethodService",
9
9
  "broadcastProtocol": "android-ime-helper-v1"
10
10
  }
@@ -0,0 +1 @@
1
+ ce370af9f1c97e7c812cb93772f864596731c10f3d0b7cadc1c88b772a062970 agent-device-android-snapshot-helper-0.20.5.apk
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "android-snapshot-helper",
3
- "version": "0.20.3",
4
- "releaseTag": "v0.20.3",
5
- "assetName": "agent-device-android-snapshot-helper-0.20.3.apk",
3
+ "version": "0.20.5",
4
+ "releaseTag": "v0.20.5",
5
+ "assetName": "agent-device-android-snapshot-helper-0.20.5.apk",
6
6
  "apkUrl": null,
7
- "sha256": "de7ecffc80d54dd1c173d3bf445ff8c9a02de2d124bcf39abeafeb5a6a08c95e",
8
- "checksumName": "agent-device-android-snapshot-helper-0.20.3.apk.sha256",
7
+ "sha256": "ce370af9f1c97e7c812cb93772f864596731c10f3d0b7cadc1c88b772a062970",
8
+ "checksumName": "agent-device-android-snapshot-helper-0.20.5.apk.sha256",
9
9
  "packageName": "com.callstack.agentdevice.snapshothelper",
10
- "versionCode": 20003,
10
+ "versionCode": 20005,
11
11
  "instrumentationRunner": "com.callstack.agentdevice.snapshothelper/.SnapshotInstrumentation",
12
12
  "minSdk": 23,
13
13
  "targetSdk": 36,
@@ -922,6 +922,49 @@ extension RunnerTests {
922
922
  x: touchPoint.x,
923
923
  y: touchPoint.y
924
924
  )
925
+ var fallback: GestureFallback?
926
+ if command.synthesized == true {
927
+ let policyKind = SynthesizedGesturePolicyKind.coordinateTap
928
+ let context = synthesizedCoordinateContext(
929
+ app: activeApp,
930
+ policy: synthesizedGesturePolicy(policyKind)
931
+ )
932
+ let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
933
+ synthesizedTapAt(
934
+ app: activeApp,
935
+ x: touchPoint.x,
936
+ y: touchPoint.y,
937
+ context: context
938
+ )
939
+ }
940
+ if case .performed = outcome {
941
+ logSynthesizedGesturePolicyDecision(
942
+ kind: policyKind,
943
+ context: context,
944
+ fallbackAttempted: false
945
+ )
946
+ if isTextEntry {
947
+ waitForTextEntryReadinessAfterTap(app: activeApp, element: element)
948
+ }
949
+ return gestureResponse(
950
+ message: match.usedNonHittableFallback
951
+ ? "tapped via non-hittable coordinate fallback"
952
+ : "tapped",
953
+ timing: timing,
954
+ frame: .touch(touchFrame),
955
+ maestroNonHittableCoordinateFallbackUsed:
956
+ command.allowNonHittableCoordinateFallback == true
957
+ ? match.usedNonHittableFallback
958
+ : nil
959
+ )
960
+ }
961
+ logSynthesizedGesturePolicyDecision(
962
+ kind: policyKind,
963
+ context: context,
964
+ fallbackAttempted: true
965
+ )
966
+ fallback = gestureFallback(strategy: "xctest-coordinate-tap", from: outcome)
967
+ }
925
968
  let (timing, outcome) = performGesture(activeApp) {
926
969
  if expectedPoint != nil || match.usedNonHittableFallback {
927
970
  // Maestro compatibility: RN E2E backdoor controls can be 1x1 and
@@ -942,6 +985,7 @@ extension RunnerTests {
942
985
  message: match.usedNonHittableFallback ? "tapped via non-hittable coordinate fallback" : "tapped",
943
986
  timing: timing,
944
987
  frame: .touch(touchFrame),
988
+ fallback: fallback,
945
989
  maestroNonHittableCoordinateFallbackUsed:
946
990
  command.allowNonHittableCoordinateFallback == true
947
991
  ? match.usedNonHittableFallback
@@ -954,7 +998,10 @@ extension RunnerTests {
954
998
  var fallback: GestureFallback?
955
999
  if command.synthesized == true {
956
1000
  let policyKind = SynthesizedGesturePolicyKind.coordinateTap
957
- let context = synthesizedCoordinateContext(policy: synthesizedGesturePolicy(policyKind))
1001
+ let context = synthesizedCoordinateContext(
1002
+ app: activeApp,
1003
+ policy: synthesizedGesturePolicy(policyKind)
1004
+ )
958
1005
  let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
959
1006
  synthesizedTapAt(app: activeApp, x: x, y: y, context: context)
960
1007
  }
@@ -1045,7 +1092,10 @@ extension RunnerTests {
1045
1092
  )
1046
1093
  }
1047
1094
  let scrollPolicyKind = SynthesizedGesturePolicyKind.scroll
1048
- guard let scrollContext = synthesizedCoordinateContext(policy: synthesizedGesturePolicy(scrollPolicyKind)) else {
1095
+ guard let scrollContext = synthesizedCoordinateContext(
1096
+ app: activeApp,
1097
+ policy: synthesizedGesturePolicy(scrollPolicyKind)
1098
+ ) else {
1049
1099
  return Response(
1050
1100
  ok: false,
1051
1101
  error: ErrorPayload(message: "scroll could not resolve a usable interaction frame")
@@ -1439,7 +1489,10 @@ extension RunnerTests {
1439
1489
  var fallback: GestureFallback?
1440
1490
  if synthesized {
1441
1491
  let durationMs = min(max(durationMs ?? 250, 16), 10000)
1442
- let context = synthesizedCoordinateContext(policy: synthesizedGesturePolicy(synthesizedPolicyKind))
1492
+ let context = synthesizedCoordinateContext(
1493
+ app: activeApp,
1494
+ policy: synthesizedGesturePolicy(synthesizedPolicyKind)
1495
+ )
1443
1496
  let (timing, outcome) = performGesture(activeApp, idleTimeout: false) {
1444
1497
  synthesizedDragAt(
1445
1498
  app: activeApp,
@@ -1495,7 +1548,7 @@ extension RunnerTests {
1495
1548
  ) -> Response? {
1496
1549
  #if os(iOS)
1497
1550
  let policy = synthesizedGesturePolicy(policyKind)
1498
- let context = context ?? synthesizedCoordinateContext(policy: policy)
1551
+ let context = context ?? synthesizedCoordinateContext(app: activeApp, policy: policy)
1499
1552
  guard let plan = axFreeSynthesizedDragPlan(
1500
1553
  app: activeApp,
1501
1554
  x: x,
@@ -617,7 +617,10 @@ extension RunnerTests {
617
617
  )
618
618
  }
619
619
  let orientation = Int(RunnerSynthesizedGesture.interfaceOrientation(forApplication: app))
620
- guard let context = context ?? synthesizedCoordinateContext(policy: synthesizedGesturePolicy(.synthesizedDrag)) else {
620
+ guard let context = context ?? synthesizedCoordinateContext(
621
+ app: app,
622
+ policy: synthesizedGesturePolicy(.synthesizedDrag)
623
+ ) else {
621
624
  return .unsupported(
622
625
  message: "synthesized coordinate drag could not resolve a finite screen frame",
623
626
  hint: "Retry after the app is foregrounded, or use a plain screenshot to choose coordinates."
@@ -680,7 +683,10 @@ extension RunnerTests {
680
683
  )
681
684
  }
682
685
  let orientation = Int(RunnerSynthesizedGesture.interfaceOrientation(forApplication: app))
683
- guard let context = context ?? synthesizedCoordinateContext(policy: synthesizedGesturePolicy(.coordinateTap)) else {
686
+ guard let context = context ?? synthesizedCoordinateContext(
687
+ app: app,
688
+ policy: synthesizedGesturePolicy(.coordinateTap)
689
+ ) else {
684
690
  return .unsupported(
685
691
  message: "synthesized coordinate tap could not resolve a finite screen frame",
686
692
  hint: "Retry after the app is foregrounded, or use a plain screenshot to choose coordinates."
@@ -852,7 +858,10 @@ extension RunnerTests {
852
858
  context: SynthesizedCoordinateContext? = nil
853
859
  ) -> SynthesizedDragPlan? {
854
860
  #if os(iOS)
855
- let context = context ?? synthesizedCoordinateContext(policy: synthesizedGesturePolicy(.synthesizedDrag))
861
+ let context = context ?? synthesizedCoordinateContext(
862
+ app: app,
863
+ policy: synthesizedGesturePolicy(.synthesizedDrag)
864
+ )
856
865
  guard x.isFinite, y.isFinite, x2.isFinite, y2.isFinite,
857
866
  let context
858
867
  else {
@@ -892,11 +901,18 @@ extension RunnerTests {
892
901
  )
893
902
  }
894
903
 
895
- func synthesizedCoordinateContext(policy: SynthesizedGesturePolicy) -> SynthesizedCoordinateContext? {
904
+ func synthesizedCoordinateContext(
905
+ app: XCUIApplication,
906
+ policy: SynthesizedGesturePolicy
907
+ ) -> SynthesizedCoordinateContext? {
896
908
  #if os(iOS)
897
909
  let health = runnerAccessibilityHealth
898
- guard let referenceFrame = synthesizedScreenshotReferenceFrame(
899
- screenshotSize: { XCUIScreen.main.screenshot().image.size }
910
+ let orientation = Int(
911
+ RunnerSynthesizedGesture.interfaceOrientation(forApplication: app)
912
+ )
913
+ guard let referenceFrame = orientedSynthesizedScreenshotReferenceFrame(
914
+ screenshotSize: XCUIScreen.main.screenshot().image.size,
915
+ interfaceOrientation: orientation
900
916
  ) else {
901
917
  return nil
902
918
  }
@@ -911,15 +927,35 @@ extension RunnerTests {
911
927
  #endif
912
928
  }
913
929
 
914
- func synthesizedScreenshotReferenceFrame(screenshotSize: () -> CGSize) -> CGRect? {
915
- let screenshotSize = screenshotSize()
930
+ func orientedSynthesizedScreenshotReferenceFrame(
931
+ screenshotSize: CGSize,
932
+ interfaceOrientation: Int
933
+ ) -> CGRect? {
934
+ // Physical iOS screenshots can retain portrait dimensions after the interface rotates,
935
+ // while accessibility frames remain in the logical landscape coordinate space.
916
936
  guard screenshotSize.width.isFinite, screenshotSize.height.isFinite,
917
937
  screenshotSize.width > 0,
918
938
  screenshotSize.height > 0
919
939
  else {
920
940
  return nil
921
941
  }
922
- return CGRect(x: 0, y: 0, width: screenshotSize.width, height: screenshotSize.height)
942
+ let isLandscape = interfaceOrientation == RunnerInterfaceOrientation.landscapeLeft
943
+ || interfaceOrientation == RunnerInterfaceOrientation.landscapeRight
944
+ let isPortrait = interfaceOrientation == RunnerInterfaceOrientation.portrait
945
+ || interfaceOrientation == RunnerInterfaceOrientation.portraitUpsideDown
946
+ let width: CGFloat
947
+ let height: CGFloat
948
+ if isLandscape {
949
+ width = max(screenshotSize.width, screenshotSize.height)
950
+ height = min(screenshotSize.width, screenshotSize.height)
951
+ } else if isPortrait {
952
+ width = min(screenshotSize.width, screenshotSize.height)
953
+ height = max(screenshotSize.width, screenshotSize.height)
954
+ } else {
955
+ width = screenshotSize.width
956
+ height = screenshotSize.height
957
+ }
958
+ return CGRect(x: 0, y: 0, width: width, height: height)
923
959
  }
924
960
 
925
961
  func synthesizedFrameAvoidingKeyboardWhenAllowed(
@@ -2,6 +2,23 @@ import XCTest
2
2
 
3
3
  private enum KeyboardDismissObservationTiming {
4
4
  static let timeout: TimeInterval = 2
5
+ // #1542: dismissing the keyboard can trigger the host app's own content-offset
6
+ // adjustment (e.g. a ScrollView correcting the inset it grew to keep a focused
7
+ // field above the keyboard). That adjustment is a separate, unsynchronized
8
+ // animation the keyboard's own `waitForNonExistence` above knows nothing about:
9
+ // the keyboard AX element can vanish well before the app settles. The very next
10
+ // command is frequently a synthesized, AX-free drag (scroll/gesture — kept
11
+ // AX-free so it still works under #1105-family AX degradation), which has no
12
+ // XCTest quiescence wait of its own, so it can land mid-animation and net to
13
+ // zero. These bounds cap the settle wait this trades in.
14
+ // A spring-driven content-offset correction can pass through a near-zero-
15
+ // velocity inflection (the top of an overshoot) that two adjacent samples
16
+ // alone cannot distinguish from true rest. Requiring 3 consecutive matching
17
+ // samples demands ~2 full sample intervals of actual stillness before
18
+ // stopping early, so a momentary pause mid-animation cannot look settled.
19
+ static let settleTimeout: TimeInterval = 2.0
20
+ static let settleSampleInterval: TimeInterval = 0.15
21
+ static let settleRequiredConsecutiveMatches: Int = 3
5
22
  }
6
23
 
7
24
  extension RunnerTests {
@@ -24,6 +41,11 @@ extension RunnerTests {
24
41
  #else
25
42
  if tapKeyboardDismissControl(app: app) {
26
43
  _ = keyboard.waitForNonExistence(timeout: KeyboardDismissObservationTiming.timeout)
44
+ waitForScreenshotStability(
45
+ timeout: KeyboardDismissObservationTiming.settleTimeout,
46
+ sampleInterval: KeyboardDismissObservationTiming.settleSampleInterval,
47
+ requiredConsecutiveMatches: KeyboardDismissObservationTiming.settleRequiredConsecutiveMatches
48
+ )
27
49
  let visible = isKeyboardVisible(app: app)
28
50
  return (wasVisible: true, dismissed: !visible, visible: visible)
29
51
  }
@@ -32,6 +54,35 @@ extension RunnerTests {
32
54
  #endif
33
55
  }
34
56
 
57
+ // AX-free on purpose (screenshot bytes, not the accessibility tree) so it holds
58
+ // under the same AX degradation the synthesized gesture lane is built to survive.
59
+ // Bounded and self-terminating: returns as soon as `requiredConsecutiveMatches`
60
+ // consecutive samples match, so an already-settled screen (the common case)
61
+ // pays close to nothing. The stopping decision itself
62
+ // (`runnerScreenshotStabilitySettled`) is a pure function of the samples taken
63
+ // so far, so it is unit-testable without a real screenshot pipeline; this loop
64
+ // is the thin, untestable I/O shell around it.
65
+ private func waitForScreenshotStability(
66
+ timeout: TimeInterval,
67
+ sampleInterval: TimeInterval,
68
+ requiredConsecutiveMatches: Int
69
+ ) {
70
+ let deadline = Date().addingTimeInterval(timeout)
71
+ var samples: [Data?] = [screenshotFingerprintForStabilityCheck()]
72
+ while Date() < deadline {
73
+ sleepFor(sampleInterval)
74
+ samples.append(screenshotFingerprintForStabilityCheck())
75
+ if runnerScreenshotStabilitySettled(samples, requiredConsecutiveMatches: requiredConsecutiveMatches) {
76
+ return
77
+ }
78
+ }
79
+ }
80
+
81
+ private func screenshotFingerprintForStabilityCheck() -> Data? {
82
+ guard let image = captureRunnerFrame() else { return nil }
83
+ return runnerPngData(for: image)
84
+ }
85
+
35
86
  func pressKeyboardReturn(app: XCUIApplication) -> (wasVisible: Bool, pressed: Bool, visible: Bool) {
36
87
  #if os(tvOS)
37
88
  return (wasVisible: false, pressed: pressTvRemote(.select), visible: false)
@@ -182,3 +233,25 @@ extension RunnerTests {
182
233
  return frame.intersects(keyboardFrame) || abs(frame.maxY - keyboardFrame.minY) <= 80
183
234
  }
184
235
  }
236
+
237
+ /// True once the `requiredConsecutiveMatches` most recent samples are all present
238
+ /// and byte-identical — i.e. the screen held still across that whole run of
239
+ /// polls, not just the last two. A spring-driven content-offset correction can
240
+ /// pass through a near-zero-velocity inflection that two adjacent samples alone
241
+ /// cannot distinguish from true rest; requiring a longer run of matches demands
242
+ /// real elapsed stillness before stopping early. `nil` entries (a screenshot
243
+ /// capture that failed) never count toward a match, so a flaky capture cannot
244
+ /// look like stability; the caller's bounded loop still terminates on its
245
+ /// deadline regardless.
246
+ func runnerScreenshotStabilitySettled(
247
+ _ samples: [Data?],
248
+ requiredConsecutiveMatches: Int
249
+ ) -> Bool {
250
+ guard requiredConsecutiveMatches >= 2, samples.count >= requiredConsecutiveMatches else {
251
+ return false
252
+ }
253
+ let window = samples.suffix(requiredConsecutiveMatches)
254
+ guard let first = window.first, let firstData = first else { return false }
255
+ return window.allSatisfy { $0 == firstData }
256
+ }
257
+
@@ -118,6 +118,7 @@ extension RunnerTests {
118
118
  }
119
119
  let before = captureNavigationFallbackVisualState()
120
120
  let context = synthesizedCoordinateContext(
121
+ app: app,
121
122
  policy: synthesizedGesturePolicy(.coordinateTap)
122
123
  )?.withReferenceFrame(frame)
123
124
  let synthesized = performGesture(app, idleTimeout: false) {
@@ -1,10 +1,11 @@
1
1
  import XCTest
2
2
 
3
- // Swift port of buildScrollGesturePlan from src/contracts/scroll-gesture.ts.
3
+ // Swift port of buildScrollGesturePlan from packages/contracts/src/scroll-gesture.ts.
4
4
  //
5
5
  // This is a deliberate two-place invariant: the daemon keeps the TS implementation (for Android,
6
6
  // recording, and reported-pixels), and the runner places the gesture with this Swift copy. The
7
- // parity test vectors at the bottom of this file mirror src/contracts/scroll-gesture.test.ts —
7
+ // parity test vectors at the bottom of this file mirror
8
+ // packages/contracts/src/scroll-gesture.test.ts —
8
9
  // if you change the math in either language, update the other and both vector sets.
9
10
  //
10
11
  // All inputs here are positive (reference dims, travel, center), so Swift's `.rounded()`
@@ -40,7 +40,7 @@ extension RunnerTests {
40
40
  ? resolvedTouchVisualizationFrame(app: activeApp, x: firstStep.x!, y: firstStep.y!)
41
41
  : nil
42
42
 
43
- let synthesizedContext = synthesizedSequenceCoordinateContext(steps: steps)
43
+ let synthesizedContext = synthesizedSequenceCoordinateContext(steps: steps, app: activeApp)
44
44
 
45
45
  let execution = assembleSequenceExecution(steps: steps) { _, step in
46
46
  performSequenceStep(step, activeApp: activeApp, synthesizedContext: synthesizedContext)
@@ -108,9 +108,15 @@ func sequenceHasSynthesizedCoordinateStep(_ steps: [SequenceStep]) -> Bool {
108
108
  }
109
109
 
110
110
  extension RunnerTests {
111
- func synthesizedSequenceCoordinateContext(steps: [SequenceStep]) -> SynthesizedCoordinateContext? {
111
+ func synthesizedSequenceCoordinateContext(
112
+ steps: [SequenceStep],
113
+ app: XCUIApplication
114
+ ) -> SynthesizedCoordinateContext? {
112
115
  guard sequenceHasSynthesizedCoordinateStep(steps) else { return nil }
113
- return synthesizedCoordinateContext(policy: synthesizedGesturePolicy(.synthesizedDrag))
116
+ return synthesizedCoordinateContext(
117
+ app: app,
118
+ policy: synthesizedGesturePolicy(.synthesizedDrag)
119
+ )
114
120
  }
115
121
 
116
122
  func logSynthesizedGesturePolicyDecision(
@@ -1 +1 @@
1
- import{t as e}from"./rolldown-runtime.js";import{_ as t,v as n}from"./sdk-contracts.js";import{T as r,j as i,v as a}from"./sdk-android-adb.js";import{c as o,i as s,o as c,t as l}from"./host-process.js";import{t as u}from"./process-lock.js";import d from"node:crypto";import f from"node:fs";import p from"node:os";import m from"node:path";const h=new Map;function g(e){let t=d.createHash(`sha256`);return t.update(m.resolve(e.stateDir)),t.update(`\0`),t.update(m.resolve(e.installDir)),`--agent-device-managed-web=${t.digest(`hex`).slice(0,16)}`}function _(e,t){let n=g(t),r=e?.trim();return r?T(r).includes(n)?r:`${r},${n}`:n}function v(e){return E(e.AGENT_BROWSER_IDLE_TIMEOUT_MS)??E(e.AGENT_DEVICE_WEB_IDLE_TIMEOUT_MS)??3e5}function y(e,t){if(e.pid!==process.pid&&D(e.command)){if(e.command.includes(g(t)))return{process:e,reason:`launch-marker`};if(O(t).some(t=>j(e,t)))return{process:e,reason:`managed-browser-home`}}}function b(e,t){let n=e.flatMap(e=>{let n=y(e,t);return n?[n]:[]});return{count:n.length,pids:n.map(e=>e.process.pid),processes:n}}async function x(e){return b(await w(),e)}async function S(e,t,n={}){let r=k([...n.openWebSessionNames??[]]);if(r.length>0)return A(`open-web-session`,{openWebSessionNames:r});let a=v(process.env),s=N(e);if(s!==void 0&&Date.now()-s<a)return A(`recent-browser-activity`,{idleTimeoutMs:a,latestActivityMs:s});let c=await w(),u=b(c,e),d=l(u.processes.map(e=>e.process.pid),c).map(e=>e.pid),f={...u,signalPids:d};return u.count===0?f:(i({level:`warn`,phase:`web_agent_browser_orphan_cleanup`,data:{reason:t,count:u.count,pids:u.pids,signalPids:d,stateDir:e.stateDir,installDir:e.installDir,matchReasons:u.processes.map(e=>e.reason)}}),await o({pids:d,termTimeoutMs:1500,killTimeoutMs:1e3}),f)}async function C(e,t={}){let n=Date.now(),r=m.resolve(e.stateDir),i=h.get(r);if(!(i!==void 0&&n-i<3e4))return h.set(r,n),await S(e,`provider-startup`,t)}async function w(){return await r(async()=>await s({timeoutMs:1500}))}function T(e){return e.split(/[,\n]/).map(e=>e.trim()).filter(Boolean)}function E(e){if(e===void 0||e.trim()===``)return;let t=Number.parseInt(e,10);return Number.isInteger(t)&&t>0?t:void 0}function D(e){return/\b(?:Google Chrome for Testing|Chrome for Testing|Chromium|chrome|chrome\.exe|chromium|chromium-browser|headless_shell)\b/i.test(e)}function O(e){return k([m.join(e.homeDir,`.agent-browser`,`browsers`),m.join(e.runtimeHomeDir,`.agent-browser`,`browsers`)])}function k(e){return[...new Set(e.filter(e=>e.length>0))]}function A(e,t){return{count:0,pids:[],processes:[],signalPids:[],skipped:{reason:e,...t}}}function j(e,t){let n=M(e.command),r=`${M(m.resolve(t)).replace(/\/+$/,``)}/`;return n.includes(r)}function M(e){return e.replace(/\\/g,`/`)}function N(e){let t=ee(e.socketDir).map(e=>P(e)).filter(e=>e!==void 0);return t.length>0?Math.max(...t):void 0}function ee(e){try{return f.readdirSync(e).map(t=>m.join(e,t))}catch{return[]}}function P(e){try{return f.statSync(e).mtimeMs}catch{return}}var F=e({doctorManagedAgentBrowser:()=>H,getManagedAgentBrowserStatus:()=>U,mapManagedAgentBrowserError:()=>$,resolveAgentBrowserTool:()=>B,setupManagedAgentBrowser:()=>V});const I=`0.27.1`,L=`agent-browser`,R=5*6e4,z=6e4;async function B(e){let t=U(e);if(t.installed)return W(t);throw Q(t)}async function V(e){let t=U(e);te(t.nodeMajor);let n=await u({lockDirPath:m.join(t.installDir,`..`,`.agent-browser-install.lock`),owner:{pid:process.pid,startTime:c(process.pid),acquiredAtMs:Date.now()},timeoutMs:R,description:`managed agent-browser setup`});try{let t=U(e);return t.installed?t:(f.mkdirSync(t.installDir,{recursive:!0}),await G(t),await K(t,[`install`],{timeoutMs:R}),await K(t,[`doctor`,`--offline`,`--quick`],{timeoutMs:z}),Y(t),U(e))}finally{await n()}}async function H(e){let t=U(e);if(!t.installed)throw Q(t);return{status:t,...await K(t,[`doctor`,`--offline`,`--quick`],{timeoutMs:z,allowFailure:!0})}}function U(e){let t=e.stateDir??process.env.AGENT_DEVICE_STATE_DIR??oe(),n=m.join(t,`tools`,`agent-browser`,I),r=Z(n),i=m.join(n,`home`),a=ne(n),o=re(n),s=ae(r)&&X(n),c=Number.parseInt(process.versions.node.split(`.`)[0]??`0`,10);return{version:I,stateDir:t,installDir:n,binaryPath:r,homeDir:i,runtimeHomeDir:a,socketDir:o,installed:s,nodeMajor:c,nodeSupported:c>=24}}function W(e){if(!e.installed)throw Q(e);return{command:e.binaryPath,env:q(e,process.env)}}async function G(e){let t=m.join(e.installDir,`package`);f.mkdirSync(t,{recursive:!0}),await a(`npm`,[`install`,`--prefix`,t,`--no-audit`,`--no-fund`,`--no-save`,`${L}@${I}`],{env:process.env,timeoutMs:R})}async function K(e,t,n){return await a(e.binaryPath,t,{allowFailure:n.allowFailure,env:q(e,process.env),timeoutMs:n.timeoutMs})}function q(e,t){return f.mkdirSync(e.homeDir,{recursive:!0}),J(e),f.mkdirSync(e.socketDir,{recursive:!0}),{...t,HOME:e.runtimeHomeDir,AGENT_BROWSER_SOCKET_DIR:e.socketDir,AGENT_BROWSER_IDLE_TIMEOUT_MS:String(v(t)),AGENT_BROWSER_ARGS:_(t.AGENT_BROWSER_ARGS,e)}}function J(e){if(e.runtimeHomeDir!==e.homeDir){f.mkdirSync(m.dirname(e.runtimeHomeDir),{recursive:!0});try{let t=f.lstatSync(e.runtimeHomeDir);if(t.isSymbolicLink()&&f.readlinkSync(e.runtimeHomeDir)===e.homeDir||t.isDirectory())return;t.isSymbolicLink()&&f.unlinkSync(e.runtimeHomeDir)}catch(e){if(!ie(e))throw e}try{f.symlinkSync(e.homeDir,e.runtimeHomeDir,`dir`)}catch{f.mkdirSync(e.runtimeHomeDir,{recursive:!0})}}}function Y(e){f.writeFileSync(m.join(e.installDir,`manifest.json`),JSON.stringify({package:L,version:I,node:process.version,installedAt:new Date().toISOString()},null,2),`utf8`)}function X(e){return f.existsSync(m.join(e,`manifest.json`))}function Z(e){let t=m.join(e,`package`);return m.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:I,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 te(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 ne(e){if(process.platform===`win32`)return m.join(e,`home`);let t=d.createHash(`sha1`).update(e).digest(`hex`).slice(0,12);return m.join(p.tmpdir(),`agent-device-web`,t)}function re(e){let t=d.createHash(`sha1`).update(e).digest(`hex`).slice(0,12);return m.join(p.tmpdir(),`adw`,t)}function ie(e){return typeof e==`object`&&!!e&&`code`in e&&e.code===`ENOENT`}function ae(e){try{return f.accessSync(e,f.constants.X_OK),!0}catch{return!1}}function oe(){return m.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{S as a,B as i,U as n,C as o,$ as r,x as s,F as t};
1
+ import{o as e}from"./png-worker-contract.js";import{_ as t,v as n}from"./sdk-contracts.js";import{T as r,j as i,v as a}from"./sdk-android-adb.js";import{c as o,i as s,o as c,t as l}from"./host-process.js";import{t as u}from"./process-lock.js";import d from"node:path";import f from"node:crypto";import p from"node:fs";import m from"node:os";const h=new Map;function g(e){let t=f.createHash(`sha256`);return t.update(d.resolve(e.stateDir)),t.update(`\0`),t.update(d.resolve(e.installDir)),`--agent-device-managed-web=${t.digest(`hex`).slice(0,16)}`}function _(e,t){let n=g(t),r=e?.trim();return r?T(r).includes(n)?r:`${r},${n}`:n}function v(e){return E(e.AGENT_BROWSER_IDLE_TIMEOUT_MS)??E(e.AGENT_DEVICE_WEB_IDLE_TIMEOUT_MS)??3e5}function y(e,t){if(e.pid!==process.pid&&D(e.command)){if(e.command.includes(g(t)))return{process:e,reason:`launch-marker`};if(O(t).some(t=>j(e,t)))return{process:e,reason:`managed-browser-home`}}}function b(e,t){let n=e.flatMap(e=>{let n=y(e,t);return n?[n]:[]});return{count:n.length,pids:n.map(e=>e.process.pid),processes:n}}async function x(e){return b(await w(),e)}async function S(e,t,n={}){let r=k([...n.openWebSessionNames??[]]);if(r.length>0)return A(`open-web-session`,{openWebSessionNames:r});let a=v(process.env),s=N(e);if(s!==void 0&&Date.now()-s<a)return A(`recent-browser-activity`,{idleTimeoutMs:a,latestActivityMs:s});let c=await w(),u=b(c,e),d=l(u.processes.map(e=>e.process.pid),c).map(e=>e.pid),f={...u,signalPids:d};return u.count===0?f:(i({level:`warn`,phase:`web_agent_browser_orphan_cleanup`,data:{reason:t,count:u.count,pids:u.pids,signalPids:d,stateDir:e.stateDir,installDir:e.installDir,matchReasons:u.processes.map(e=>e.reason)}}),await o({pids:d,termTimeoutMs:1500,killTimeoutMs:1e3}),f)}async function C(e,t={}){let n=Date.now(),r=d.resolve(e.stateDir),i=h.get(r);if(!(i!==void 0&&n-i<3e4))return h.set(r,n),await S(e,`provider-startup`,t)}async function w(){return await r(async()=>await s({timeoutMs:1500}))}function T(e){return e.split(/[,\n]/).map(e=>e.trim()).filter(Boolean)}function E(e){if(e===void 0||e.trim()===``)return;let t=Number.parseInt(e,10);return Number.isInteger(t)&&t>0?t:void 0}function D(e){return/\b(?:Google Chrome for Testing|Chrome for Testing|Chromium|chrome|chrome\.exe|chromium|chromium-browser|headless_shell)\b/i.test(e)}function O(e){return k([d.join(e.homeDir,`.agent-browser`,`browsers`),d.join(e.runtimeHomeDir,`.agent-browser`,`browsers`)])}function k(e){return[...new Set(e.filter(e=>e.length>0))]}function A(e,t){return{count:0,pids:[],processes:[],signalPids:[],skipped:{reason:e,...t}}}function j(e,t){let n=M(e.command),r=`${M(d.resolve(t)).replace(/\/+$/,``)}/`;return n.includes(r)}function M(e){return e.replace(/\\/g,`/`)}function N(e){let t=ee(e.socketDir).map(e=>P(e)).filter(e=>e!==void 0);return t.length>0?Math.max(...t):void 0}function ee(e){try{return p.readdirSync(e).map(t=>d.join(e,t))}catch{return[]}}function P(e){try{return p.statSync(e).mtimeMs}catch{return}}var F=e({doctorManagedAgentBrowser:()=>H,getManagedAgentBrowserStatus:()=>U,mapManagedAgentBrowserError:()=>$,resolveAgentBrowserTool:()=>B,setupManagedAgentBrowser:()=>V});const I=`0.27.1`,L=`agent-browser`,R=5*6e4,z=6e4;async function B(e){let t=U(e);if(t.installed)return W(t);throw Q(t)}async function V(e){let t=U(e);te(t.nodeMajor);let n=await u({lockDirPath:d.join(t.installDir,`..`,`.agent-browser-install.lock`),owner:{pid:process.pid,startTime:c(process.pid),acquiredAtMs:Date.now()},timeoutMs:R,description:`managed agent-browser setup`});try{let t=U(e);return t.installed?t:(p.mkdirSync(t.installDir,{recursive:!0}),await G(t),await K(t,[`install`],{timeoutMs:R}),await K(t,[`doctor`,`--offline`,`--quick`],{timeoutMs:z}),Y(t),U(e))}finally{await n()}}async function H(e){let t=U(e);if(!t.installed)throw Q(t);return{status:t,...await K(t,[`doctor`,`--offline`,`--quick`],{timeoutMs:z,allowFailure:!0})}}function U(e){let t=e.stateDir??process.env.AGENT_DEVICE_STATE_DIR??oe(),n=d.join(t,`tools`,`agent-browser`,I),r=Z(n),i=d.join(n,`home`),a=ne(n),o=re(n),s=ae(r)&&X(n),c=Number.parseInt(process.versions.node.split(`.`)[0]??`0`,10);return{version:I,stateDir:t,installDir:n,binaryPath:r,homeDir:i,runtimeHomeDir:a,socketDir:o,installed:s,nodeMajor:c,nodeSupported:c>=24}}function W(e){if(!e.installed)throw Q(e);return{command:e.binaryPath,env:q(e,process.env)}}async function G(e){let t=d.join(e.installDir,`package`);p.mkdirSync(t,{recursive:!0}),await a(`npm`,[`install`,`--prefix`,t,`--no-audit`,`--no-fund`,`--no-save`,`${L}@${I}`],{env:process.env,timeoutMs:R})}async function K(e,t,n){return await a(e.binaryPath,t,{allowFailure:n.allowFailure,env:q(e,process.env),timeoutMs:n.timeoutMs})}function q(e,t){return p.mkdirSync(e.homeDir,{recursive:!0}),J(e),p.mkdirSync(e.socketDir,{recursive:!0}),{...t,HOME:e.runtimeHomeDir,AGENT_BROWSER_SOCKET_DIR:e.socketDir,AGENT_BROWSER_IDLE_TIMEOUT_MS:String(v(t)),AGENT_BROWSER_ARGS:_(t.AGENT_BROWSER_ARGS,e)}}function J(e){if(e.runtimeHomeDir!==e.homeDir){p.mkdirSync(d.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(!ie(e))throw e}try{p.symlinkSync(e.homeDir,e.runtimeHomeDir,`dir`)}catch{p.mkdirSync(e.runtimeHomeDir,{recursive:!0})}}}function Y(e){p.writeFileSync(d.join(e.installDir,`manifest.json`),JSON.stringify({package:L,version:I,node:process.version,installedAt:new Date().toISOString()},null,2),`utf8`)}function X(e){return p.existsSync(d.join(e,`manifest.json`))}function Z(e){let t=d.join(e,`package`);return d.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:I,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 te(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 ne(e){if(process.platform===`win32`)return d.join(e,`home`);let t=f.createHash(`sha1`).update(e).digest(`hex`).slice(0,12);return d.join(m.tmpdir(),`agent-device-web`,t)}function re(e){let t=f.createHash(`sha1`).update(e).digest(`hex`).slice(0,12);return d.join(m.tmpdir(),`adw`,t)}function ie(e){return typeof e==`object`&&!!e&&`code`in e&&e.code===`ENOENT`}function ae(e){try{return p.accessSync(e,p.constants.X_OK),!0}catch{return!1}}function oe(){return d.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{S as a,B as i,U as n,C as o,$ as r,x as s,F as t};