peachsim 0.1.1619 → 0.1.1625

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/README.md +35 -5
  2. package/cli/app-fonts.ts +2 -1
  3. package/cli/bridge-flow-runner.ts +222 -175
  4. package/cli/cloud-client.ts +81 -48
  5. package/cli/commands/box/search-command.ts +3 -2
  6. package/cli/commands/box.ts +10 -29
  7. package/cli/commands/control.ts +108 -43
  8. package/cli/commands/inspect/settling.ts +57 -20
  9. package/cli/commands/maestro-compare.ts +11 -32
  10. package/cli/commands/platform.ts +74 -17
  11. package/cli/commands/record.ts +20 -15
  12. package/cli/commands/upload.ts +5 -1
  13. package/cli/dev-build-freshness.ts +37 -2
  14. package/cli/drivers/playwright-sim-host.ts +18 -6
  15. package/cli/maestro-accessibility.ts +53 -0
  16. package/cli/maestro-compare.ts +24 -0
  17. package/cli/outbound-endpoints.ts +27 -8
  18. package/cli/parent-pid.ts +0 -9
  19. package/cli/remote-simulators.ts +112 -0
  20. package/cli/remoteSimulatorContract.ts +21 -0
  21. package/detox/expectations.ts +5 -1
  22. package/detox/index.ts +133 -98
  23. package/detox/motion-change.cjs +37 -0
  24. package/detox/motion-change.d.cts +16 -0
  25. package/detox/navigation-lifecycle.ts +22 -4
  26. package/dist-lib/agent-daemon-client.cjs +3 -2
  27. package/dist-lib/agent-events.cjs +1 -1
  28. package/dist-lib/attached-projects.cjs +1 -1
  29. package/dist-lib/auth/shared-session.cjs +3 -2
  30. package/dist-lib/backend-origin.cjs +1 -1
  31. package/dist-lib/beta.cjs +1 -1
  32. package/dist-lib/beta.mjs +1 -1
  33. package/dist-lib/bridge-constants.cjs +1 -1
  34. package/dist-lib/bridge-contract-input.cjs +1 -1
  35. package/dist-lib/bridge-contract-input.mjs +1 -1
  36. package/dist-lib/bridge-contract.cjs +1 -1
  37. package/dist-lib/bridge-contract.mjs +1 -1
  38. package/dist-lib/bundle-channel.cjs +1528 -937
  39. package/dist-lib/capture-contract.cjs +1 -1
  40. package/dist-lib/capture-contract.mjs +1 -1
  41. package/dist-lib/cloud-contract.cjs +1 -1
  42. package/dist-lib/cloud-contract.mjs +1 -1
  43. package/dist-lib/cloud-test-args.cjs +1 -1
  44. package/dist-lib/cloud-test-args.mjs +1 -1
  45. package/dist-lib/cloud.cjs +4 -3
  46. package/dist-lib/cloud.mjs +4 -3
  47. package/dist-lib/config.cjs +5 -4
  48. package/dist-lib/config.mjs +5 -4
  49. package/dist-lib/detox/index.cjs +1021 -364
  50. package/dist-lib/dev-bundle-resolution.cjs +51 -23
  51. package/dist-lib/expo-swift-bundle.cjs +1351 -804
  52. package/dist-lib/expo-swift-metadata.cjs +5 -4
  53. package/dist-lib/expo-swift-metadata.mjs +5 -4
  54. package/dist-lib/home-paths.cjs +1 -1
  55. package/dist-lib/host/bridge-host.cjs +2475 -1343
  56. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  57. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  58. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  59. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  60. package/dist-lib/host/websocket-proxy.cjs +1 -1
  61. package/dist-lib/index.cjs +1269 -300
  62. package/dist-lib/jump-to-source-babel.cjs +1 -1
  63. package/dist-lib/jump-to-source-native.cjs +1 -1
  64. package/dist-lib/kotlin-host.cjs +1 -1
  65. package/dist-lib/menu.cjs +3 -2
  66. package/dist-lib/menu.mjs +3 -2
  67. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  68. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  69. package/dist-lib/metro-production-bundle.cjs +5 -4
  70. package/dist-lib/metro-production-bundle.mjs +5 -4
  71. package/dist-lib/metro.cjs +806 -398
  72. package/dist-lib/native-app-identity-project.cjs +729 -21
  73. package/dist-lib/native-app-identity-project.mjs +728 -21
  74. package/dist-lib/native-app-identity.cjs +232 -9
  75. package/dist-lib/native-app-identity.mjs +230 -8
  76. package/dist-lib/native-firebase-metadata.cjs +5 -4
  77. package/dist-lib/native-firebase-metadata.mjs +5 -4
  78. package/dist-lib/plist.cjs +1 -1
  79. package/dist-lib/plist.mjs +1 -1
  80. package/dist-lib/profiles.cjs +1 -1
  81. package/dist-lib/public-brand.cjs +3 -2
  82. package/dist-lib/react-native-host-modules.cjs +4 -1
  83. package/dist-lib/react-native-host-modules.mjs +4 -1
  84. package/dist-lib/scripts/dev-server-scanner.cjs +94 -28
  85. package/dist-lib/sdk.cjs +89 -24
  86. package/dist-lib/sdk.mjs +89 -24
  87. package/dist-lib/skills.cjs +4471 -2394
  88. package/dist-lib/swift-compile.cjs +336 -145
  89. package/dist-lib/swift-compile.mjs +350 -150
  90. package/dist-lib/swift-package.cjs +1 -1
  91. package/dist-lib/swift-package.mjs +1 -1
  92. package/dist-lib/swift-submit.cjs +1 -1
  93. package/dist-lib/swift-submit.mjs +1 -1
  94. package/dist-lib/swift-toolchain.cjs +1 -1
  95. package/dist-lib/swift.cjs +1 -1
  96. package/dist-lib/vite-swift.cjs +891 -557
  97. package/dist-lib/vite.cjs +68 -14
  98. package/dist-lib/worklet-executable.cjs +25 -2
  99. package/dist-lib/worklet-executable.mjs +25 -2
  100. package/package.json +2 -2
  101. package/scripts/dev-server-scanner.ts +33 -13
  102. package/src/admitted-mac-build.ts +34 -8
  103. package/src/android-app-identity.ts +146 -0
  104. package/src/android-autolinking.ts +75 -0
  105. package/src/android-string-value.ts +36 -0
  106. package/src/android-strings.ts +5 -36
  107. package/src/androidAutolinkingContract.ts +40 -0
  108. package/src/bundleGlobalFooter.ts +26 -7
  109. package/src/cloud.ts +2 -2
  110. package/src/codex-client.ts +3 -2
  111. package/src/dev-bundle-resolution.ts +58 -25
  112. package/src/expo-kotlin-host.ts +6 -36
  113. package/src/expo-swift-bundle.ts +17 -12
  114. package/src/expo-swift-host.ts +477 -173
  115. package/src/expoSwiftContract.ts +6 -0
  116. package/src/host/bridge-host.ts +7 -55
  117. package/src/host/staged-host-files.ts +29 -2
  118. package/src/installed-package-versions.ts +54 -14
  119. package/src/launchMetadataContract.ts +24 -0
  120. package/src/metro-plugin.ts +37 -42
  121. package/src/native-app-identity-project.ts +118 -3
  122. package/src/native-app-identity.ts +103 -11
  123. package/src/nitro-compile.ts +2 -5
  124. package/src/nitro-source.ts +147 -63
  125. package/src/process-group.ts +90 -60
  126. package/src/process-output.ts +0 -9
  127. package/src/public-brand.ts +1 -0
  128. package/src/react-native-host-modules.ts +3 -0
  129. package/src/sim-client.ts +6 -4
  130. package/src/swift-compile.ts +185 -31
  131. package/src/swift-distribution.lock.json +37 -11
  132. package/src/swift-distribution.ts +61 -9
  133. package/src/swift-host.ts +58 -27
  134. package/src/swift-objc-standin.ts +13 -0
  135. package/src/swift-standin-emit.ts +1 -1
  136. package/src/swift-standins.ts +17 -5
  137. package/src/vite-plugin.ts +8 -19
  138. package/src/worklet-executable.ts +30 -3
  139. package/src/worklets-babel.ts +35 -14
  140. package/src/xcode-project.ts +10 -0
package/detox/index.ts CHANGED
@@ -378,7 +378,7 @@ export async function waitForSootsimShellReady(
378
378
  async function waitForSootsimWorkletsIdle(page: Page, timeout = 3000): Promise<void> {
379
379
  const result = await page.evaluate(
380
380
  async ({ timeout }) => {
381
- const getPeerStats = (window as any).__sootsimTest?.getPeerStats
381
+ const getPeerStats = window.__sootsimTest?.getPeerStats
382
382
  if (typeof getPeerStats !== 'function') {
383
383
  return { settled: true, skipped: true }
384
384
  }
@@ -387,7 +387,9 @@ async function waitForSootsimWorkletsIdle(page: Page, timeout = 3000): Promise<v
387
387
  const start = Date.now()
388
388
  const deadline = start + timeout
389
389
  const requiredStableMs = 120
390
- let lastPending: number | null = null
390
+ let lastPending: number | null | undefined = null
391
+ let completedReads = 0
392
+ let lastReply: string | null = null
391
393
 
392
394
  while (Date.now() < deadline) {
393
395
  let timer: ReturnType<typeof setTimeout> | undefined
@@ -404,6 +406,8 @@ async function waitForSootsimWorkletsIdle(page: Page, timeout = 3000): Promise<v
404
406
  })
405
407
  if (stats === undefined) break
406
408
  if (stats === null) return { settled: true, elapsed: Date.now() - start }
409
+ completedReads++
410
+ lastReply = JSON.stringify({ elapsedMs: Date.now() - start, stats })
407
411
  lastPending = stats.pendingRunWorklets
408
412
  // the runtime records its last drain. bridge latency cannot erase an
409
413
  // idle interval or turn a missing stats reply into a zero count.
@@ -421,42 +425,80 @@ async function waitForSootsimWorkletsIdle(page: Page, timeout = 3000): Promise<v
421
425
  await sleep(25)
422
426
  }
423
427
 
424
- // no reply at all: ask the shell alone, so a silent tenant and a silent
425
- // shell read differently
426
- let shellAnswered: boolean | null = null
428
+ const waited = Date.now() - start
429
+ // freeze the completed observations before the separate realm probes.
430
+ // a later reply cannot replace what the original deadline observed.
431
+ const observation = { completedReads, lastReply }
432
+ // no reply at all: ask each realm alone, and time this page's own timer,
433
+ // so a silent tenant, a silent shell and a starved page read differently
434
+ let realms: string | null = null
427
435
  if (lastPending === null) {
428
- const shell = (window as any).SootSim?.bridges?.shellHost
429
- shellAnswered = shell
430
- ? await Promise.race([
431
- shell.callTestBridge('getPeerStats', false).then(
432
- () => true,
433
- () => false,
434
- ),
435
- sleep(1000).then(() => false),
436
+ const probe = async (bridge: unknown) => {
437
+ if (
438
+ !bridge ||
439
+ typeof bridge !== 'object' ||
440
+ !('callTestBridge' in bridge) ||
441
+ typeof bridge.callTestBridge !== 'function'
442
+ ) {
443
+ return { status: 'missing-bridge' }
444
+ }
445
+ const callTestBridge = bridge.callTestBridge.bind(bridge)
446
+ const asked = Date.now()
447
+ let timer: ReturnType<typeof setTimeout> | undefined
448
+ try {
449
+ return await Promise.race([
450
+ Promise.resolve()
451
+ .then(() => callTestBridge('getPeerStats', false))
452
+ .then(
453
+ (stats) => ({
454
+ status: 'answered',
455
+ elapsedMs: Date.now() - asked,
456
+ stats,
457
+ }),
458
+ (error: unknown) => ({
459
+ status: 'rejected',
460
+ elapsedMs: Date.now() - asked,
461
+ error: error instanceof Error ? error.message : String(error),
462
+ }),
463
+ ),
464
+ new Promise<{ status: 'no-reply'; elapsedMs: number }>((resolve) => {
465
+ timer = setTimeout(
466
+ () => resolve({ status: 'no-reply', elapsedMs: Date.now() - asked }),
467
+ 1000,
468
+ )
469
+ }),
436
470
  ])
437
- : null
471
+ } finally {
472
+ if (timer !== undefined) clearTimeout(timer)
473
+ }
474
+ }
475
+ const timerAsked = Date.now()
476
+ const [tenant, shell, pageTimerLateMs] = await Promise.all([
477
+ probe(window.__sootsimRenderHost),
478
+ probe(window.SootSim?.bridges?.shellHost),
479
+ sleep(0).then(() => Date.now() - timerAsked),
480
+ ])
481
+ realms = JSON.stringify({
482
+ phase: 'after-deadline',
483
+ tenant,
484
+ shell,
485
+ pageTimerLateMs,
486
+ })
438
487
  }
439
488
  return {
440
489
  settled: false,
441
- elapsed: Date.now() - start,
490
+ elapsed: waited,
442
491
  pending: lastPending,
443
- shellAnswered,
492
+ realms,
493
+ observation,
444
494
  }
445
495
  },
446
496
  { timeout },
447
497
  )
448
498
  if (!result?.settled) {
449
- const pending =
450
- result?.pending ??
451
- `no peer stats reply; shell alone ${
452
- result?.shellAnswered == null
453
- ? 'has no bridge'
454
- : result.shellAnswered
455
- ? 'answered within 1000ms'
456
- : 'silent for 1000ms'
457
- }`
499
+ const pending = result?.pending ?? `no peer stats reply; ${result?.realms}`
458
500
  throw new Error(
459
- `sootsim worklet runtime did not settle after ${result?.elapsed ?? timeout}ms (pending=${pending})`,
501
+ `sootsim worklet runtime did not settle after ${result?.elapsed ?? timeout}ms (pending=${pending}; observation=${JSON.stringify(result?.observation)})`,
460
502
  )
461
503
  }
462
504
  }
@@ -756,67 +798,62 @@ async function nodeOffsetToPage(
756
798
  return sootsimToPage(page, center.x + dx, center.y + dy)
757
799
  }
758
800
 
759
- async function getDefaultTapPoint(
760
- page: Page,
761
- nodeInfo: any,
762
- ): Promise<{ x: number; y: number }> {
763
- const resolvedTarget = await page.evaluate(
764
- (nodeId) => window.__sootsimTest?.resolveTapTarget?.(nodeId) ?? null,
765
- nodeInfo.nodeId,
766
- )
767
- const center =
768
- resolvedTarget &&
769
- typeof resolvedTarget.cx === 'number' &&
770
- typeof resolvedTarget.cy === 'number'
771
- ? { x: resolvedTarget.cx, y: resolvedTarget.cy }
772
- : getNodeCenter(nodeInfo)
773
- const frame = nodeInfo.visibleFrame ?? {
774
- x: nodeInfo.absolutePosition?.x ?? nodeInfo.layout?.x ?? 0,
775
- y: nodeInfo.absolutePosition?.y ?? nodeInfo.layout?.y ?? 0,
776
- width: nodeInfo.layout?.width ?? 0,
777
- height: nodeInfo.layout?.height ?? 0,
778
- }
779
- const viewport = await readSootsimInteractiveViewport(page)
780
- const fullFrameTolerance = 0.5
781
- // the resolved center is the matched frame's center unless the frame is clipped.
782
- const canTapCenter =
783
- typeof nodeInfo.absolutePosition?.x === 'number' &&
784
- typeof nodeInfo.absolutePosition?.y === 'number' &&
785
- typeof nodeInfo.layout?.width === 'number' &&
786
- typeof nodeInfo.layout?.height === 'number' &&
787
- Math.abs(frame.x - nodeInfo.absolutePosition.x) <= fullFrameTolerance &&
788
- Math.abs(frame.y - nodeInfo.absolutePosition.y) <= fullFrameTolerance &&
789
- frame.width >= nodeInfo.layout.width - fullFrameTolerance &&
790
- frame.height >= nodeInfo.layout.height - fullFrameTolerance &&
791
- center.x >= 0 &&
792
- center.y >= 0 &&
793
- center.x < viewport.width &&
794
- center.y < viewport.height
795
- if (canTapCenter) return center
796
-
797
- const left = Math.max(0, frame.x)
798
- const top = Math.max(0, frame.y)
799
- const right = Math.min(viewport.width, frame.x + frame.width)
800
- const bottom = Math.min(viewport.height, frame.y + frame.height)
801
-
802
- if (right <= left || bottom <= top) {
803
- return center
804
- }
805
-
806
- return {
807
- x: left + (right - left) / 2,
808
- y: top + (bottom - top) / 2,
809
- }
810
- }
801
+ type TapPoint =
802
+ | { x: number; y: number }
803
+ | {
804
+ nodeId: number
805
+ viewport: { width: number; height: number }
806
+ offset?: { x?: number; y?: number }
807
+ }
811
808
 
812
- async function dispatchTapAtSootsimPoint(
813
- page: Page,
814
- x: number,
815
- y: number,
816
- token?: string,
817
- ) {
809
+ async function dispatchTapAtSootsimPoint(page: Page, point: TapPoint, token?: string) {
818
810
  await page.evaluate(
819
- async ({ x, y, token }) => {
811
+ async ({ point, token }) => {
812
+ let x: number
813
+ let y: number
814
+ if ('nodeId' in point) {
815
+ // resolve after the viewport read, in the browser task that dispatches
816
+ // the pointer. a host round trip here can outlive a startup relayout.
817
+ const resolved = await window.__sootsimTest!.resolveTapTarget(point.nodeId)
818
+ if (!resolved) throw new Error(`element disappeared before tap: ${point.nodeId}`)
819
+ const node = resolved.match
820
+ if (point.offset) {
821
+ x = node.absolutePosition.x + (point.offset.x ?? node.layout.width / 2)
822
+ y = node.absolutePosition.y + (point.offset.y ?? node.layout.height / 2)
823
+ } else {
824
+ x = resolved.cx
825
+ y = resolved.cy
826
+ const frame = node.visibleFrame ?? {
827
+ x: node.absolutePosition.x,
828
+ y: node.absolutePosition.y,
829
+ width: node.layout.width,
830
+ height: node.layout.height,
831
+ }
832
+ const { viewport } = point
833
+ const tolerance = 0.5
834
+ const canTapCenter =
835
+ Math.abs(frame.x - node.absolutePosition.x) <= tolerance &&
836
+ Math.abs(frame.y - node.absolutePosition.y) <= tolerance &&
837
+ frame.width >= node.layout.width - tolerance &&
838
+ frame.height >= node.layout.height - tolerance &&
839
+ x >= 0 &&
840
+ y >= 0 &&
841
+ x < viewport.width &&
842
+ y < viewport.height
843
+ if (!canTapCenter) {
844
+ const left = Math.max(0, frame.x)
845
+ const top = Math.max(0, frame.y)
846
+ const right = Math.min(viewport.width, frame.x + frame.width)
847
+ const bottom = Math.min(viewport.height, frame.y + frame.height)
848
+ if (right > left && bottom > top) {
849
+ x = left + (right - left) / 2
850
+ y = top + (bottom - top) / 2
851
+ }
852
+ }
853
+ }
854
+ } else {
855
+ ;({ x, y } = point)
856
+ }
820
857
  const interactTap = window.SootSim?.bridges?.interact?.tap
821
858
  if (typeof interactTap === 'function') {
822
859
  if (token) window.__sootsimAnchorTimedInput?.(token)
@@ -832,7 +869,7 @@ async function dispatchTapAtSootsimPoint(
832
869
  if (token) window.__sootsimAnchorTimedInput?.(token)
833
870
  await tap(x, y)
834
871
  },
835
- { x, y, token },
872
+ { point, token },
836
873
  )
837
874
  }
838
875
 
@@ -1087,17 +1124,13 @@ function createSootSimElement(matcher: Matcher): SootSimElement {
1087
1124
  `Cannot perform action due to constraint failure: element not visible (${describeNodeVisibility(node)}): ${JSON.stringify(matcher)}`,
1088
1125
  )
1089
1126
  }
1090
- const defaultPoint = point ? null : await getDefaultTapPoint(page, node)
1091
- const targetSootSimX =
1092
- typeof point?.x === 'number'
1093
- ? node.absolutePosition.x + point.x
1094
- : (defaultPoint?.x ?? node.absolutePosition.x + node.layout.width / 2)
1095
- const targetSootSimY =
1096
- typeof point?.y === 'number'
1097
- ? node.absolutePosition.y + point.y
1098
- : (defaultPoint?.y ?? node.absolutePosition.y + node.layout.height / 2)
1127
+ const viewport = await readSootsimInteractiveViewport(page)
1099
1128
  const captureToken = (page as any).__sootsimCaptureToken
1100
- await dispatchTapAtSootsimPoint(page, targetSootSimX, targetSootSimY, captureToken)
1129
+ await dispatchTapAtSootsimPoint(
1130
+ page,
1131
+ { nodeId: node.nodeId, viewport, offset: point },
1132
+ captureToken,
1133
+ )
1101
1134
  if (_synchronizationEnabled) {
1102
1135
  await waitForSootsimIdle(page)
1103
1136
  }
@@ -1540,8 +1573,10 @@ function createSootSimElement(matcher: Matcher): SootSimElement {
1540
1573
  const captureToken = (page as any).__sootsimCaptureToken
1541
1574
  await dispatchTapAtSootsimPoint(
1542
1575
  page,
1543
- tapTarget.target.absolutePosition.x + tapTarget.target.layout.width / 2,
1544
- tapTarget.target.absolutePosition.y + tapTarget.target.layout.height / 2,
1576
+ {
1577
+ x: tapTarget.target.absolutePosition.x + tapTarget.target.layout.width / 2,
1578
+ y: tapTarget.target.absolutePosition.y + tapTarget.target.layout.height / 2,
1579
+ },
1545
1580
  captureToken,
1546
1581
  )
1547
1582
  },
@@ -102,6 +102,42 @@ function measureProofMotionChange(input) {
102
102
  })
103
103
  }
104
104
 
105
+ // a target sampled at every display frame (each gap at most one 60fps interval)
106
+ // legitimately includes held input and settled frames whose pairs show no
107
+ // change. such a sequence proves motion only when at least six pairs (100ms of
108
+ // display frames) clear the minimum: a one-frame flash moves two pairs and a
109
+ // static capture none, and both fail. a sparser sequence samples distinct
110
+ // stages, so each pair must clear it.
111
+ const DENSE_SAMPLE_MAX_GAP_MS = 1000 / 60 + 1
112
+ const DENSE_SAMPLE_MINIMUM_MOVING_PAIRS = 6
113
+
114
+ function evaluateMotionTransitions(transitions) {
115
+ const measured = transitions.every(
116
+ (transition) =>
117
+ transition.dimensionsMatch &&
118
+ transition.changePercent !== null &&
119
+ transition.changePixels !== null &&
120
+ transition.totalPixels !== null,
121
+ )
122
+ const moving = transitions.filter((transition) => transition.passedMinimum).length
123
+ const dense =
124
+ transitions.length > DENSE_SAMPLE_MINIMUM_MOVING_PAIRS &&
125
+ transitions.every(
126
+ (transition) =>
127
+ transition.toOffsetMs - transition.fromOffsetMs <= DENSE_SAMPLE_MAX_GAP_MS,
128
+ )
129
+ return {
130
+ dense,
131
+ moving,
132
+ passed:
133
+ transitions.length > 0 &&
134
+ measured &&
135
+ (dense
136
+ ? moving >= DENSE_SAMPLE_MINIMUM_MOVING_PAIRS
137
+ : moving === transitions.length),
138
+ }
139
+ }
140
+
105
141
  const DISPLAY_LINK_INTERVAL_MS = 1000 / 60
106
142
  const DISCRETE_FRAME_TIMING_TOLERANCE_MS = 1000 / 60 + 1000 / 600
107
143
 
@@ -361,6 +397,7 @@ module.exports = {
361
397
  MOTION_CHANGE_DEFINITION,
362
398
  measureMotionChange,
363
399
  measureProofMotionChange,
400
+ evaluateMotionTransitions,
364
401
  DISCRETE_FRAME_TIMING_TOLERANCE_MS,
365
402
  parseDiscreteFrameCadence,
366
403
  classifyDiscreteFrame,
@@ -34,6 +34,22 @@ export function measureProofMotionChange(
34
34
  input: Omit<MotionChangeInput, 'definition' | 'installGlobal'>,
35
35
  ): MotionChangeMeasurement
36
36
 
37
+ export type MotionTransitionSample = {
38
+ changePercent: number | null
39
+ changePixels: number | null
40
+ dimensionsMatch: boolean
41
+ fromOffsetMs: number
42
+ passedMinimum: boolean
43
+ toOffsetMs: number
44
+ totalPixels: number | null
45
+ }
46
+
47
+ export function evaluateMotionTransitions(transitions: MotionTransitionSample[]): {
48
+ dense: boolean
49
+ moving: number
50
+ passed: boolean
51
+ }
52
+
37
53
  export type DiscreteFrameAsset = {
38
54
  fixturePath: string
39
55
  fixtureSha256: string
@@ -83,12 +83,28 @@ const activeNavigations = new WeakMap<Page, ActiveNavigation>()
83
83
  const lastNavigations = new WeakMap<Page, ActiveNavigation>()
84
84
  let nextNavigationIdentity = 0
85
85
 
86
+ const persistedDiagnostics = new WeakSet<PageDiagnostic>()
87
+
88
+ function persistDiagnostic(diagnostic: PageDiagnostic) {
89
+ if (persistedDiagnostics.has(diagnostic)) return
90
+ persistedDiagnostics.add(diagnostic)
91
+ writeAttemptEvent(diagnostic)
92
+ }
93
+
94
+ // a run that dies at the launch marker never reaches the failure message that
95
+ // prints this buffer, so a guest error writes the console tail that led to it
96
+ // into events.jsonl. passing runs emit no guest error and write nothing.
86
97
  function recordPageDiagnostic(page: Page, diagnostic: PageDiagnostic, persist = false) {
87
98
  const entries = pageDiagnostics.get(page)
88
99
  if (!entries) return
89
100
  entries.push(diagnostic)
90
101
  if (entries.length > DIAGNOSTIC_LIMIT) entries.shift()
91
- if (persist) writeAttemptEvent(diagnostic)
102
+ if (persist) persistDiagnostic(diagnostic)
103
+ }
104
+
105
+ function recordGuestError(page: Page, diagnostic: PageDiagnostic) {
106
+ recordPageDiagnostic(page, diagnostic)
107
+ for (const entry of pageDiagnostics.get(page) ?? []) persistDiagnostic(entry)
92
108
  }
93
109
 
94
110
  export function isTopLevelDocumentRequest(page: Page, request: Request) {
@@ -147,15 +163,17 @@ function attachPageDiagnostics(page: Page) {
147
163
  if (pageDiagnostics.has(page)) return
148
164
  pageDiagnostics.set(page, [])
149
165
  page.on('console', (message) => {
150
- recordPageDiagnostic(page, {
166
+ const diagnostic: PageDiagnostic = {
151
167
  capturedAt: new Date().toISOString(),
152
168
  level: message.type(),
153
169
  message: message.text(),
154
170
  type: 'page-console',
155
- })
171
+ }
172
+ if (message.type() === 'error') recordGuestError(page, diagnostic)
173
+ else recordPageDiagnostic(page, diagnostic)
156
174
  })
157
175
  page.on('pageerror', (error) => {
158
- recordPageDiagnostic(page, {
176
+ recordGuestError(page, {
159
177
  capturedAt: new Date().toISOString(),
160
178
  message: error.message,
161
179
  type: 'page-error',
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1625 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -59,7 +59,8 @@ var peachPublicBrand = Object.freeze({
59
59
  packageName: "peachsim",
60
60
  commandName: "peach",
61
61
  domain: "peachsim.com",
62
- origin: "https://peachsim.com"
62
+ origin: "https://peachsim.com",
63
+ boxOrigin: "https://rnx-box.contrast.dev"
63
64
  });
64
65
  var rnxPublicBrand = peachPublicBrand;
65
66
 
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1625 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1625 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1625 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -69,7 +69,8 @@ var peachPublicBrand = Object.freeze({
69
69
  packageName: "peachsim",
70
70
  commandName: "peach",
71
71
  domain: "peachsim.com",
72
- origin: "https://peachsim.com"
72
+ origin: "https://peachsim.com",
73
+ boxOrigin: "https://rnx-box.contrast.dev"
73
74
  });
74
75
 
75
76
  // src/auth/shared-session.ts
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1625 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
package/dist-lib/beta.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1625 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
package/dist-lib/beta.mjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1625 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
 
3
3
  // src/beta.ts
4
4
  var IS_BETA = true;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1625 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1625 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1625 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
 
3
3
  // src/bridge-contract.ts
4
4
  var SIM_LONG_PRESS_MAX_MS = 5e3;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1625 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
  let __sootsim_import_meta_url = '', __sootsim_import_meta_dirname = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; __sootsim_import_meta_dirname = __dirname; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1619 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1625 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
 
3
3
  // src/bridge-contract.ts
4
4
  var REFUSED_PERFORM_STEP_TYPES = [