peachsim 0.1.1635 → 0.1.1641

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 (142) hide show
  1. package/README.md +10 -1
  2. package/cli/app-config.ts +14 -5
  3. package/cli/app-stubs.ts +96 -22
  4. package/cli/bridge-flow-runner.ts +2 -2
  5. package/cli/cloud-client.ts +60 -40
  6. package/cli/cloud-session.ts +16 -0
  7. package/cli/commands/assert.ts +17 -4
  8. package/cli/commands/box.ts +33 -19
  9. package/cli/commands/doctor.ts +37 -20
  10. package/cli/commands/flow.ts +1 -1
  11. package/cli/commands/inspect/actions.ts +3 -2
  12. package/cli/commands/inspect/core.ts +0 -8
  13. package/cli/commands/inspect/shared.ts +9 -4
  14. package/cli/commands/inspect.ts +65 -61
  15. package/cli/commands/platform.ts +2 -1
  16. package/cli/commands/preview.ts +1 -1
  17. package/cli/commands/record.ts +154 -68
  18. package/cli/commands/swift.ts +18 -4
  19. package/cli/commands/three-mode.ts +4 -38
  20. package/cli/commands/upload.ts +55 -49
  21. package/cli/drivers/playwright-sim-host.ts +65 -42
  22. package/cli/flow-live-status.ts +0 -2
  23. package/cli/main.ts +2 -2
  24. package/cli/outbound-endpoints.ts +7 -5
  25. package/cli/pause-aware-timeout.ts +4 -0
  26. package/cli/ws-bridge.ts +19 -16
  27. package/detox/attempt-events.cjs +99 -6
  28. package/detox/attempt-events.d.cts +7 -0
  29. package/detox/expectations.ts +7 -3
  30. package/detox/index.ts +259 -139
  31. package/detox/jest-environment.cjs +5 -1
  32. package/detox/motion-change.cjs +170 -21
  33. package/detox/motion-change.d.cts +32 -0
  34. package/detox/navigation-lifecycle.ts +35 -3
  35. package/dist-lib/agent-daemon-client.cjs +1 -1
  36. package/dist-lib/agent-events.cjs +1 -1
  37. package/dist-lib/attached-projects.cjs +1 -1
  38. package/dist-lib/auth/shared-session.cjs +1 -1
  39. package/dist-lib/backend-origin.cjs +17 -7
  40. package/dist-lib/beta.cjs +1 -1
  41. package/dist-lib/beta.mjs +1 -1
  42. package/dist-lib/bridge-constants.cjs +1 -1
  43. package/dist-lib/bridge-contract-input.cjs +1 -1
  44. package/dist-lib/bridge-contract-input.mjs +1 -1
  45. package/dist-lib/bridge-contract.cjs +1 -1
  46. package/dist-lib/bridge-contract.mjs +1 -1
  47. package/dist-lib/bundle-channel.cjs +5014 -2268
  48. package/dist-lib/capture-contract.cjs +1 -1
  49. package/dist-lib/capture-contract.mjs +1 -1
  50. package/dist-lib/cloud-contract.cjs +1 -1
  51. package/dist-lib/cloud-contract.mjs +1 -1
  52. package/dist-lib/cloud-test-args.cjs +1 -1
  53. package/dist-lib/cloud-test-args.mjs +1 -1
  54. package/dist-lib/cloud.cjs +1 -1
  55. package/dist-lib/cloud.mjs +1 -1
  56. package/dist-lib/config.cjs +1 -1
  57. package/dist-lib/config.mjs +1 -1
  58. package/dist-lib/detox/index.cjs +1036 -166
  59. package/dist-lib/detox/jest-environment.cjs +5 -1
  60. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  61. package/dist-lib/expo-swift-bundle.cjs +4613 -1963
  62. package/dist-lib/expo-swift-metadata.cjs +9 -7
  63. package/dist-lib/expo-swift-metadata.mjs +9 -7
  64. package/dist-lib/home-paths.cjs +1 -1
  65. package/dist-lib/host/bridge-host.cjs +58072 -16400
  66. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  67. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  68. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  69. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  70. package/dist-lib/host/websocket-proxy.cjs +1 -1
  71. package/dist-lib/index.cjs +2790 -143
  72. package/dist-lib/jump-to-source-babel.cjs +1 -1
  73. package/dist-lib/jump-to-source-native.cjs +1 -1
  74. package/dist-lib/kotlin-host.cjs +1 -1
  75. package/dist-lib/menu.cjs +1 -1
  76. package/dist-lib/menu.mjs +1 -1
  77. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  78. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  79. package/dist-lib/metro-production-bundle.cjs +1 -1
  80. package/dist-lib/metro-production-bundle.mjs +1 -1
  81. package/dist-lib/metro.cjs +2579 -95
  82. package/dist-lib/native-app-identity-project.cjs +243 -53
  83. package/dist-lib/native-app-identity-project.mjs +243 -53
  84. package/dist-lib/native-app-identity.cjs +29 -30
  85. package/dist-lib/native-app-identity.mjs +29 -30
  86. package/dist-lib/native-firebase-metadata.cjs +1 -1
  87. package/dist-lib/native-firebase-metadata.mjs +1 -1
  88. package/dist-lib/plist.cjs +1 -1
  89. package/dist-lib/plist.mjs +1 -1
  90. package/dist-lib/profiles.cjs +1 -1
  91. package/dist-lib/public-brand.cjs +1 -1
  92. package/dist-lib/react-native-host-modules.cjs +2 -2
  93. package/dist-lib/react-native-host-modules.mjs +2 -2
  94. package/dist-lib/scripts/dev-server-scanner.cjs +8 -6
  95. package/dist-lib/sdk.cjs +4 -3
  96. package/dist-lib/sdk.mjs +4 -3
  97. package/dist-lib/skills.cjs +66475 -23097
  98. package/dist-lib/swift-compile.cjs +452 -260
  99. package/dist-lib/swift-compile.mjs +451 -252
  100. package/dist-lib/swift-package.cjs +1 -1
  101. package/dist-lib/swift-package.mjs +1 -1
  102. package/dist-lib/swift-submit.cjs +14 -7
  103. package/dist-lib/swift-submit.mjs +14 -7
  104. package/dist-lib/swift-toolchain.cjs +316 -41
  105. package/dist-lib/swift.cjs +1 -1
  106. package/dist-lib/vite-swift.cjs +8188 -5636
  107. package/dist-lib/vite.cjs +2857 -12
  108. package/dist-lib/worklet-executable.cjs +14 -2
  109. package/dist-lib/worklet-executable.mjs +14 -2
  110. package/package.json +1 -1
  111. package/src/backend-origin.ts +21 -6
  112. package/src/expo-swift-host.ts +5 -2
  113. package/src/expoKotlinContract.ts +4 -7
  114. package/src/expoSwiftContract.ts +4 -6
  115. package/src/host/staged-host-files.ts +9 -0
  116. package/src/native-app-identity-project.ts +1 -4
  117. package/src/native-app-identity.ts +1 -4
  118. package/src/nativeAppIconContract.ts +1 -4
  119. package/src/output-lock-stdio.ts +36 -0
  120. package/src/react-native-bridge-modules.ts +106 -78
  121. package/src/react-native-host-modules.ts +4 -1
  122. package/src/settings-bundle.ts +1 -4
  123. package/src/swift-compile.ts +224 -139
  124. package/src/swift-distribution.lock.json +23 -12
  125. package/src/swift-distribution.ts +103 -4
  126. package/src/swift-host.ts +95 -30
  127. package/src/swift-icu.ts +16 -10
  128. package/src/swift-objc-compile.ts +547 -0
  129. package/src/swift-objc-metadata.ts +204 -0
  130. package/src/swift-objc-sdk.ts +883 -19
  131. package/src/swift-objc-standin.ts +126 -14
  132. package/src/swift-package-manifest.ts +42 -1
  133. package/src/swift-packages.ts +35 -21
  134. package/src/swift-root-lock.ts +153 -0
  135. package/src/swift-standin-emit.ts +40 -7
  136. package/src/swift-standins.ts +404 -138
  137. package/src/swift-submit.ts +18 -7
  138. package/src/swift-toolchain.ts +378 -46
  139. package/src/swiftContract.ts +12 -0
  140. package/src/worker-system-chunks.ts +11 -12
  141. package/src/worklet-executable.ts +19 -1
  142. package/src/xcode-project.ts +280 -33
@@ -50,11 +50,15 @@ module.exports = class PeachEnvironment extends TestEnvironment {
50
50
  }
51
51
  }
52
52
 
53
- async handleTestEvent(event) {
53
+ async handleTestEvent(event, state) {
54
54
  if (event.name === 'hook_failure') {
55
55
  this.controller.abort(new Error('Peach test hook failed'))
56
56
  try {
57
57
  await this.global.__peachDetoxCleanup?.()
58
+ } catch (error) {
59
+ // circus already recorded the hook failure. a cleanup failure belongs
60
+ // beside it, rather than escaping this handler and replacing the result.
61
+ state.unhandledErrors.push(error)
58
62
  } finally {
59
63
  this.releaseAdmission?.()
60
64
  this.releaseAdmission = null
@@ -106,30 +106,35 @@ function measureProofMotionChange(input) {
106
106
  // exactly still, so only oracle changes of at least this much need a sootsim
107
107
  // match.
108
108
  const PAIRED_MOTION_MINIMUM_CHANGE_PERCENT = 0.3
109
- // a lone change below this after a still transition is a settle blip, which
110
- // the two targets land a frame or two apart, so it needs no match.
109
+ // the last oracle change, when it is below this and follows a still
110
+ // transition, is a settle blip, which the two targets land a frame or two
111
+ // apart, so it needs no match. only that one blip is exempt.
111
112
  const SETTLE_BLIP_MAXIMUM_CHANGE_PERCENT = 0.6
112
113
 
113
114
  // a present or dismiss records one second (60 transitions); a gesture ladder
114
115
  // records four. a gesture jumps at its touch cadence and each capture phases
115
116
  // that cadence differently, so a gesture is graded by total travel instead of
116
- // by frame, and the native driver plays the finger path up to ten frames ahead
117
- // of the sootsim one, so the whole gesture's travel is judged, not its timing.
117
+ // by frame. travel is judged over the whole gesture and again over each quarter
118
+ // so one jump or a lagging release cannot pass on its total.
118
119
  const GESTURE_MOTION_MIN_TRANSITIONS = 120
119
120
  const GESTURE_TRAVEL_RATIO_RANGE = Object.freeze({ min: 0.8, max: 1.25 })
121
+ const GESTURE_QUARTER_TRAVEL_RATIO_RANGE = Object.freeze({ min: 0.5, max: 2 })
120
122
 
121
123
  // a sampled motion proves movement as a pair: the oracle moves in at least one
122
124
  // transition. a one second motion also needs sootsim to move within one frame
123
125
  // of every transition where the oracle changed by the paired minimum; a hold
124
126
  // where the oracle holds needs no sootsim movement, while a static sootsim
125
127
  // against a moving oracle fails. a gesture needs sootsim's total travel within
126
- // a range of the oracle's. transitions pair by their stage labels; every
127
- // measurement must exist.
128
+ // a range of the oracle's. both lanes hold the same unique stage-label
129
+ // transitions in the same order; every measurement must exist.
128
130
  function evaluateMotionTransitions(transitions, oracleTarget) {
129
131
  const key = (transition) => `${transition.fromLabel}\u0000${transition.toLabel}`
130
132
  const oracle = transitions.filter((transition) => transition.target === oracleTarget)
131
133
  const sootsim = transitions.filter((transition) => transition.target === 'sootsim')
132
- const sootsimByKey = new Map(sootsim.map((transition) => [key(transition), transition]))
134
+ const paired =
135
+ oracle.length === sootsim.length &&
136
+ new Set(oracle.map(key)).size === oracle.length &&
137
+ oracle.every((transition, index) => key(transition) === key(sootsim[index]))
133
138
  const measured = transitions.every(
134
139
  (transition) =>
135
140
  transition.dimensionsMatch &&
@@ -137,38 +142,64 @@ function evaluateMotionTransitions(transitions, oracleTarget) {
137
142
  transition.changePixels !== null &&
138
143
  transition.totalPixels !== null,
139
144
  )
140
- const moving = oracle.filter((transition) => transition.passedMinimum)
141
145
  const travel = (list) =>
142
146
  list
143
147
  .filter(
144
148
  (transition) => transition.changePercent >= PAIRED_MOTION_MINIMUM_CHANGE_PERCENT,
145
149
  )
146
150
  .reduce((total, transition) => total + transition.changePercent, 0)
147
- const sootsimMovesNear = (index) =>
148
- [index - 1, index, index + 1].some(
149
- (neighbour) => sootsim[neighbour]?.passedMinimum === true,
150
- )
151
- const isSettleBlip = (transition, index) =>
152
- transition.changePercent < SETTLE_BLIP_MAXIMUM_CHANGE_PERCENT &&
153
- !(oracle[index - 1]?.passedMinimum ?? true)
151
+ const movesNear = (list, index) =>
152
+ [index - 1, index, index + 1].some((neighbour) => list[neighbour]?.passedMinimum === true)
153
+ const pairedChange = (transition) =>
154
+ transition.changePercent >= PAIRED_MOTION_MINIMUM_CHANGE_PERCENT
155
+ const settleBlip = (list) => {
156
+ const index = list.findLastIndex(pairedChange)
157
+ return index >= 0 &&
158
+ list[index].changePercent < SETTLE_BLIP_MAXIMUM_CHANGE_PERCENT &&
159
+ !(list[index - 1]?.passedMinimum ?? true)
160
+ ? index
161
+ : -1
162
+ }
163
+ const oracleBlip = settleBlip(oracle)
164
+ const sootsimBlip = settleBlip(sootsim)
165
+ // the oracle moves only where it changed by the paired minimum; drift and
166
+ // its settle blip alone are not a motion.
167
+ const moving = oracle.filter(
168
+ (transition, index) => pairedChange(transition) && index !== oracleBlip,
169
+ )
154
170
  const within = (actual, expected, range) =>
155
171
  actual >= expected * range.min && actual <= expected * range.max
172
+ const quarter = (list, index) =>
173
+ list.slice(
174
+ Math.floor((list.length * index) / 4),
175
+ Math.floor((list.length * (index + 1)) / 4),
176
+ )
156
177
  const follows =
157
178
  oracle.length >= GESTURE_MOTION_MIN_TRANSITIONS
158
- ? within(travel(sootsim), travel(oracle), GESTURE_TRAVEL_RATIO_RANGE)
179
+ ? within(travel(sootsim), travel(oracle), GESTURE_TRAVEL_RATIO_RANGE) &&
180
+ [0, 1, 2, 3].every((index) =>
181
+ within(
182
+ travel(quarter(sootsim, index)),
183
+ travel(quarter(oracle, index)),
184
+ GESTURE_QUARTER_TRAVEL_RATIO_RANGE,
185
+ ),
186
+ )
159
187
  : oracle.every(
160
188
  (transition, index) =>
161
- !transition.passedMinimum ||
162
- transition.changePercent < PAIRED_MOTION_MINIMUM_CHANGE_PERCENT ||
163
- isSettleBlip(transition, index) ||
164
- (sootsimByKey.has(key(transition)) && sootsimMovesNear(index)),
189
+ !pairedChange(transition) ||
190
+ index === oracleBlip ||
191
+ movesNear(sootsim, index),
192
+ ) &&
193
+ sootsim.every(
194
+ (transition, index) =>
195
+ !pairedChange(transition) || index === sootsimBlip || movesNear(oracle, index),
165
196
  )
166
197
  return {
167
198
  moving: moving.length,
168
199
  passed:
169
200
  measured &&
170
201
  oracle.length > 0 &&
171
- sootsim.length === oracle.length &&
202
+ paired &&
172
203
  moving.length > 0 &&
173
204
  follows,
174
205
  }
@@ -429,8 +460,126 @@ function evaluateDiscreteFrameTrace(input) {
429
460
  return { mode: 'playing', anchorMs: anchor.atMs, holds }
430
461
  }
431
462
 
463
+ // a motion publishes when its transitions pair and its pixels agree at the
464
+ // ends and at a few places in between. the first and last stage must pass their
465
+ // region budget; a ladder of at least ten stages also needs two intermediate
466
+ // stages that pass while the oracle is still moving into them. dense samples
467
+ // may move less than the pairing bar; filtered pixel change away from both
468
+ // endpoint images still proves intermediate motion. a settled tail frame
469
+ // proves nothing about the motion and one frame of timing shift
470
+ // fails a stage outright. the remaining intermediate stages are reported as
471
+ // room to improve, never as a failure. a sparse ladder samples too coarsely to
472
+ // ask for intermediates. a repeating motion also needs every cycle's period
473
+ // within one frame of the oracle's, since re-anchoring each cycle would
474
+ // otherwise hide a lane that runs fast or slow.
475
+ const PUBLICATION_DENSE_LADDER_MIN_STAGES = 10
476
+ const PUBLICATION_MIN_INTERMEDIATE_PASSES = 2
477
+
478
+ // one onset per cycle of a repeating animation on the lane's own clock: the
479
+ // transition drive, then each `cycle` anchor in declaration order. ios reports
480
+ // recording seconds (the transition's triggerInVideoSec, each anchor's
481
+ // detection.atSec), android its producer clock (triggerProducerSeconds, each
482
+ // anchor's producerSeconds), and peach virtual clock milliseconds (driveStart,
483
+ // each anchor's own receipt).
484
+ function motionCycleOnsets(event, eventPath) {
485
+ if (!Array.isArray(event.anchors)) return []
486
+ const cycles = event.anchors.filter(
487
+ (anchor) => typeof anchor === 'object' && anchor !== null && anchor.cycle === true,
488
+ )
489
+ if (cycles.length === 0) return []
490
+ const finite = (value, label) => {
491
+ if (typeof value !== 'number' || !Number.isFinite(value))
492
+ throw new Error(`motion event cycle ${label} has no onset time: ${eventPath}`)
493
+ return value
494
+ }
495
+ const android = event.captureMode === 'android-screenrecord-sequence'
496
+ const peach = typeof event.driveStart === 'object' && event.driveStart !== null
497
+ const basisMs = peach
498
+ ? finite(event.driveStart.virtualTimestampMs, 'transition')
499
+ : android
500
+ ? finite(event.triggerProducerSeconds, 'transition') * 1000
501
+ : finite(event.triggerInVideoSec, 'transition') * 1000
502
+ return [
503
+ { id: 'transition', atMs: basisMs },
504
+ ...cycles.map((anchor) => {
505
+ if (typeof anchor.id !== 'string')
506
+ throw new Error(`motion event cycle anchor has no id: ${eventPath}`)
507
+ const atMs = peach
508
+ ? finite(anchor.virtualTimestampMs, anchor.id)
509
+ : android
510
+ ? finite(anchor.producerSeconds, anchor.id) * 1000
511
+ : finite(anchor.detection?.atSec, anchor.id) * 1000
512
+ return { id: anchor.id, atMs }
513
+ }),
514
+ ]
515
+ }
516
+
517
+ // one display frame: the period a native repeat cycle may differ from
518
+ // peach's before re-anchoring each cycle would hide real drift
519
+ const MOTION_CYCLE_PERIOD_TOLERANCE_MS = 1000 / 60
520
+
521
+ function motionCyclePeriods(reference, sootsim) {
522
+ const ids = (onsets) => onsets.map((onset) => onset.id).join(',')
523
+ if (ids(reference) !== ids(sootsim))
524
+ throw new Error(
525
+ `motion cycle anchors differ between lanes: reference [${ids(reference)}], sootsim [${ids(sootsim)}]`,
526
+ )
527
+ return reference.slice(1).map((onset, index) => {
528
+ const referenceMs = onset.atMs - reference[index].atMs
529
+ const sootsimMs = sootsim[index + 1].atMs - sootsim[index].atMs
530
+ return {
531
+ id: onset.id,
532
+ passed: Math.abs(referenceMs - sootsimMs) <= MOTION_CYCLE_PERIOD_TOLERANCE_MS,
533
+ referenceMs,
534
+ sootsimMs,
535
+ }
536
+ })
537
+ }
538
+
539
+ function evaluateMotionPublication(stages, transitions, oracleTarget, cycleOnsets) {
540
+ const cyclePeriods = motionCyclePeriods(cycleOnsets.reference, cycleOnsets.sootsim)
541
+ const intermediates = stages.slice(1, -1)
542
+ const intermediatePassed = intermediates.filter((stage) => stage.passedThreshold).length
543
+ const stagesByLabel = new Map(stages.map((stage) => [stage.label, stage]))
544
+ const oracleMovingInto = new Set(
545
+ transitions
546
+ .filter(
547
+ (transition) =>
548
+ transition.target === oracleTarget &&
549
+ (transition.changePercent >= PAIRED_MOTION_MINIMUM_CHANGE_PERCENT ||
550
+ (transition.changePixels > 0 &&
551
+ stagesByLabel.get(transition.toLabel)?.oracleFromFirstPercent >=
552
+ PAIRED_MOTION_MINIMUM_CHANGE_PERCENT &&
553
+ stagesByLabel.get(transition.toLabel)?.oracleFromLastPercent >=
554
+ PAIRED_MOTION_MINIMUM_CHANGE_PERCENT)),
555
+ )
556
+ .map((transition) => transition.toLabel),
557
+ )
558
+ const movingPassed = intermediates.filter(
559
+ (stage) => stage.passedThreshold && oracleMovingInto.has(stage.label),
560
+ ).length
561
+ const needsIntermediates = stages.length >= PUBLICATION_DENSE_LADDER_MIN_STAGES
562
+ const first = stages[0]
563
+ const last = stages[stages.length - 1]
564
+ return {
565
+ passed:
566
+ stages.length > 0 &&
567
+ first.passedThreshold &&
568
+ last.passedThreshold &&
569
+ (!needsIntermediates || movingPassed >= PUBLICATION_MIN_INTERMEDIATE_PASSES) &&
570
+ cyclePeriods.every((period) => period.passed) &&
571
+ evaluateMotionTransitions(transitions, oracleTarget).passed,
572
+ cyclePeriods,
573
+ intermediateCount: intermediates.length,
574
+ intermediatePassed,
575
+ movingPassed,
576
+ }
577
+ }
578
+
432
579
  module.exports = {
433
580
  MOTION_CHANGE_DEFINITION,
581
+ evaluateMotionPublication,
582
+ motionCycleOnsets,
434
583
  measureMotionChange,
435
584
  measureProofMotionChange,
436
585
  evaluateMotionTransitions,
@@ -55,6 +55,38 @@ export function evaluateMotionTransitions(
55
55
  passed: boolean
56
56
  }
57
57
 
58
+ export type MotionCycleOnset = { id: string; atMs: number }
59
+
60
+ export type MotionCyclePeriod = {
61
+ id: string
62
+ passed: boolean
63
+ referenceMs: number
64
+ sootsimMs: number
65
+ }
66
+
67
+ export function motionCycleOnsets(
68
+ event: Record<string, unknown>,
69
+ eventPath: string,
70
+ ): MotionCycleOnset[]
71
+
72
+ export function evaluateMotionPublication(
73
+ stages: readonly {
74
+ label: string
75
+ passedThreshold: boolean
76
+ oracleFromFirstPercent: number | null
77
+ oracleFromLastPercent: number | null
78
+ }[],
79
+ transitions: MotionTransitionSample[],
80
+ oracleTarget: string,
81
+ cycleOnsets: { reference: MotionCycleOnset[]; sootsim: MotionCycleOnset[] },
82
+ ): {
83
+ passed: boolean
84
+ cyclePeriods: MotionCyclePeriod[]
85
+ intermediateCount: number
86
+ intermediatePassed: number
87
+ movingPassed: number
88
+ }
89
+
58
90
  export type DiscreteFrameAsset = {
59
91
  fixturePath: string
60
92
  fixtureSha256: string
@@ -1,3 +1,4 @@
1
+ import { readPeachConfigFromSearchParams } from '../src/config'
1
2
  import { writeAttemptEvent } from './attempt-events.cjs'
2
3
  import type { Page, Request, Response } from 'playwright-core'
3
4
 
@@ -77,6 +78,18 @@ type PageDiagnostic =
77
78
  url: string
78
79
  }
79
80
 
81
+ // developer config belongs in the browser, never in persisted diagnostics.
82
+ const developerToken = readPeachConfigFromSearchParams(
83
+ new URL(process.env.PEACH_URL || 'http://localhost').searchParams,
84
+ )?.mapKit?.token
85
+
86
+ function redactDiagnosticText(text: string): string {
87
+ const withoutConfig = text.replace(/([?&]peachConfig=)[^&\s"'<>]+/g, '$1[redacted]')
88
+ return developerToken
89
+ ? withoutConfig.replaceAll(developerToken, '[redacted]')
90
+ : withoutConfig
91
+ }
92
+
80
93
  const DIAGNOSTIC_LIMIT = 40
81
94
  const pageDiagnostics = new WeakMap<Page, PageDiagnostic[]>()
82
95
  const activeNavigations = new WeakMap<Page, ActiveNavigation>()
@@ -95,6 +108,21 @@ function persistDiagnostic(diagnostic: PageDiagnostic) {
95
108
  // prints this buffer, so a guest error writes the console tail that led to it
96
109
  // into events.jsonl. passing runs emit no guest error and write nothing.
97
110
  function recordPageDiagnostic(page: Page, diagnostic: PageDiagnostic, persist = false) {
111
+ if ('url' in diagnostic) diagnostic.url = redactDiagnosticText(diagnostic.url)
112
+ if ('navigationUrl' in diagnostic)
113
+ diagnostic.navigationUrl = redactDiagnosticText(diagnostic.navigationUrl)
114
+ if ('message' in diagnostic)
115
+ diagnostic.message = redactDiagnosticText(diagnostic.message)
116
+ if ('errorText' in diagnostic && diagnostic.errorText)
117
+ diagnostic.errorText = redactDiagnosticText(diagnostic.errorText)
118
+ if ('pageState' in diagnostic && diagnostic.pageState) {
119
+ if ('url' in diagnostic.pageState)
120
+ diagnostic.pageState.url = redactDiagnosticText(diagnostic.pageState.url)
121
+ if ('evaluateError' in diagnostic.pageState)
122
+ diagnostic.pageState.evaluateError = redactDiagnosticText(
123
+ diagnostic.pageState.evaluateError,
124
+ )
125
+ }
98
126
  const entries = pageDiagnostics.get(page)
99
127
  if (!entries) return
100
128
  entries.push(diagnostic)
@@ -229,7 +257,7 @@ async function readSootsimPageState(
229
257
  page: Page,
230
258
  ): Promise<SootsimPageState | { evaluateError: string }> {
231
259
  try {
232
- return await page.evaluate(() => {
260
+ const state = await page.evaluate(() => {
233
261
  const bootMarks = Reflect.get(window, '__sootsimBootMarks')
234
262
  return {
235
263
  bodyText: document.body?.innerText?.slice(0, 500) ?? '',
@@ -248,6 +276,8 @@ async function readSootsimPageState(
248
276
  url: window.location.href,
249
277
  }
250
278
  })
279
+ state.url = redactDiagnosticText(state.url)
280
+ return state
251
281
  } catch (error) {
252
282
  return {
253
283
  evaluateError: error instanceof Error ? error.message : String(error),
@@ -277,7 +307,9 @@ export async function describeSootsimBridgeFailure(
277
307
  `cause: ${cause instanceof Error ? cause.message : String(cause)}`,
278
308
  `page: ${JSON.stringify(state)}`,
279
309
  `recent page diagnostics:\n${JSON.stringify(getSootsimNavigationDiagnostics(page))}`,
280
- ].join('\n'),
310
+ ]
311
+ .map(redactDiagnosticText)
312
+ .join('\n'),
281
313
  )
282
314
  }
283
315
 
@@ -289,7 +321,7 @@ export async function navigateSootsimPage(
289
321
  attachPageDiagnostics(page)
290
322
  const navigation = {
291
323
  ...identity,
292
- navigationUrl: url,
324
+ navigationUrl: redactDiagnosticText(url),
293
325
  navigationId: ++nextNavigationIdentity,
294
326
  }
295
327
  activeNavigations.set(page, navigation)
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1635 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1641 | (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.1635 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1641 | (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.1635 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1641 | (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.1635 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1641 | (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.1635 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1641 | (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;
@@ -24,12 +24,14 @@ var backend_origin_exports = {};
24
24
  __export(backend_origin_exports, {
25
25
  DEFAULT_CONTRAST_BACKEND_ORIGIN: () => DEFAULT_CONTRAST_BACKEND_ORIGIN,
26
26
  LOOPBACK_PROBE_HOSTNAMES: () => LOOPBACK_PROBE_HOSTNAMES,
27
+ PEACHSIM_NATIVE_HOST: () => PEACHSIM_NATIVE_HOST,
27
28
  buildBrowserAssetProxyUrl: () => buildBrowserAssetProxyUrl,
28
29
  buildLoopbackProbeBaseUrl: () => buildLoopbackProbeBaseUrl,
29
30
  isBrowserAssetProxyUrl: () => isBrowserAssetProxyUrl,
30
31
  isContrastBackendHost: () => isContrastBackendHost,
31
32
  isContrastBackendOrigin: () => isContrastBackendOrigin,
32
33
  isLoopbackHost: () => isLoopbackHost,
34
+ isLoopbackOrigin: () => isLoopbackOrigin,
33
35
  isTrustedBridgeRequest: () => isTrustedBridgeRequest,
34
36
  loopbackOriginOf: () => loopbackOriginOf,
35
37
  normalizeHostname: () => normalizeHostname,
@@ -51,6 +53,13 @@ function isLoopbackHost(hostname) {
51
53
  const normalized = normalizeHostname(hostname);
52
54
  return normalized === "localhost" || normalized.endsWith(".localhost") || normalized === "0.0.0.0" || normalized === "::1" || /^127(?:\.\d{1,3}){3}$/.test(normalized);
53
55
  }
56
+ function isLoopbackOrigin(origin) {
57
+ try {
58
+ return isLoopbackHost(new URL(origin).hostname);
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
54
63
  function isTrustedBridgeRequest(headers) {
55
64
  const origin = headers.origin;
56
65
  if (origin === void 0) return true;
@@ -88,9 +97,10 @@ function isBrowserAssetProxyUrl(value) {
88
97
  return false;
89
98
  }
90
99
  }
100
+ var PEACHSIM_NATIVE_HOST = "native.peachsim.com";
91
101
  function isContrastBackendHost(hostname) {
92
102
  const normalized = normalizeHostname(hostname);
93
- return isLoopbackHost(normalized) || normalized === "contrast.dev" || normalized.endsWith(".contrast.dev");
103
+ return isLoopbackHost(normalized) || normalized === "contrast.dev" || normalized.endsWith(".contrast.dev") || normalized === PEACHSIM_NATIVE_HOST;
94
104
  }
95
105
  function isContrastBackendOrigin(origin) {
96
106
  try {
@@ -99,14 +109,12 @@ function isContrastBackendOrigin(origin) {
99
109
  return false;
100
110
  }
101
111
  }
102
- function resolveContrastBackendOrigin(currentOrigin) {
103
- const origin = currentOrigin?.trim();
112
+ function resolveContrastBackendOrigin(currentOrigin, search) {
113
+ const origin = new URLSearchParams(search ?? "").get("backend")?.trim() || currentOrigin?.trim();
104
114
  if (!origin) return DEFAULT_CONTRAST_BACKEND_ORIGIN;
105
115
  try {
106
116
  const parsed = new URL(origin);
107
- if (isLoopbackHost(parsed.hostname) || isContrastBackendHost(parsed.hostname)) {
108
- return parsed.origin;
109
- }
117
+ if (isContrastBackendHost(parsed.hostname)) return parsed.origin;
110
118
  } catch {
111
119
  }
112
120
  return DEFAULT_CONTRAST_BACKEND_ORIGIN;
@@ -115,12 +123,14 @@ function resolveContrastBackendOrigin(currentOrigin) {
115
123
  0 && (module.exports = {
116
124
  DEFAULT_CONTRAST_BACKEND_ORIGIN,
117
125
  LOOPBACK_PROBE_HOSTNAMES,
126
+ PEACHSIM_NATIVE_HOST,
118
127
  buildBrowserAssetProxyUrl,
119
128
  buildLoopbackProbeBaseUrl,
120
129
  isBrowserAssetProxyUrl,
121
130
  isContrastBackendHost,
122
131
  isContrastBackendOrigin,
123
132
  isLoopbackHost,
133
+ isLoopbackOrigin,
124
134
  isTrustedBridgeRequest,
125
135
  loopbackOriginOf,
126
136
  normalizeHostname,
package/dist-lib/beta.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! peach v0.1.1635 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1641 | (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.1635 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1641 | (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.1635 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1641 | (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.1635 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1641 | (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.1635 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1641 | (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.1635 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1641 | (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.1635 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
1
+ /*! peach v0.1.1641 | (c) 2026 Tamagui LLC | Proprietary; see LICENSE */
2
2
 
3
3
  // src/bridge-contract.ts
4
4
  var REFUSED_PERFORM_STEP_TYPES = [