peachsim 0.1.1640 → 0.1.1644
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/cli/app-bundle-resources.ts +57 -0
- package/cli/app-stubs.ts +38 -13
- package/cli/cloud-session.ts +16 -0
- package/cli/commands/assert.ts +17 -4
- package/cli/commands/box.ts +33 -19
- package/cli/commands/control.ts +12 -0
- package/cli/commands/doctor.ts +24 -15
- package/cli/commands/inspect/actions.ts +3 -2
- package/cli/commands/inspect/core.ts +0 -8
- package/cli/commands/inspect.ts +0 -14
- package/cli/commands/platform.ts +2 -1
- package/cli/commands/record.ts +64 -28
- package/cli/commands/swift.ts +10 -2
- package/cli/commands/three-mode.ts +4 -38
- package/cli/commands/upload.ts +8 -8
- package/cli/drivers/playwright.ts +39 -0
- package/cli/flow-live-status.ts +0 -2
- package/cli/main.ts +1 -1
- package/cli/outbound-endpoints.ts +4 -2
- package/cli/ws-bridge.ts +0 -6
- package/detox/attempt-events.cjs +41 -5
- package/detox/index.ts +155 -109
- package/detox/jest-environment.cjs +5 -1
- package/detox/motion-change.cjs +7 -5
- package/detox/motion-change.d.cts +1 -0
- package/detox/motion-leg-starts.cjs +20 -0
- package/detox/motion-leg-starts.d.cts +8 -0
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +12 -3
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/bundle-channel.cjs +2671 -1833
- package/dist-lib/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/config.mjs +1 -1
- package/dist-lib/detox/index.cjs +578 -130
- package/dist-lib/detox/jest-environment.cjs +5 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/expo-swift-bundle.cjs +2374 -1554
- package/dist-lib/expo-swift-metadata.cjs +1 -1
- package/dist-lib/expo-swift-metadata.mjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +2968 -2141
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +132 -57
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/kotlin-host.cjs +1 -1
- package/dist-lib/menu.cjs +14 -6
- package/dist-lib/menu.mjs +14 -6
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +120 -53
- package/dist-lib/native-app-identity-project.cjs +96 -44
- package/dist-lib/native-app-identity-project.mjs +95 -44
- package/dist-lib/native-app-identity.cjs +1 -1
- package/dist-lib/native-app-identity.mjs +1 -1
- package/dist-lib/native-firebase-metadata.cjs +1 -1
- package/dist-lib/native-firebase-metadata.mjs +1 -1
- package/dist-lib/plist.cjs +1 -1
- package/dist-lib/plist.mjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +5 -3
- package/dist-lib/sdk.mjs +5 -3
- package/dist-lib/skills.cjs +3932 -2868
- package/dist-lib/swift-compile.cjs +587 -374
- package/dist-lib/swift-compile.mjs +595 -369
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +8 -3
- package/dist-lib/swift-submit.mjs +8 -3
- package/dist-lib/swift-toolchain.cjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +2263 -1467
- package/dist-lib/vite.cjs +1 -1
- package/dist-lib/worklet-executable.cjs +1 -1
- package/dist-lib/worklet-executable.mjs +1 -1
- package/package.json +1 -1
- package/src/appBundleResourcesContract.ts +51 -0
- package/src/backend-origin.ts +11 -1
- package/src/host/bridge-host.ts +17 -11
- package/src/host/staged-host-files.ts +28 -4
- package/src/launchMetadataContract.ts +1 -0
- package/src/menu.ts +12 -4
- package/src/native-app-identity-project.ts +47 -38
- package/src/react-native-bridge-modules.ts +89 -76
- package/src/runtime-delivery.ts +6 -7
- package/src/swift-compile.ts +150 -112
- package/src/swift-compiler-digest.ts +5 -3
- package/src/swift-distribution.lock.json +9 -8
- package/src/swift-host.ts +6 -4
- package/src/swift-objc-compile.ts +416 -194
- package/src/swift-objc-metadata.ts +204 -0
- package/src/swift-objc-sdk.ts +195 -2
- package/src/swift-package-manifest.ts +6 -3
- package/src/swift-root-lock.ts +302 -0
- package/src/swift-submit.ts +15 -2
- package/src/swiftContract.ts +12 -0
- package/src/xcode-project.ts +77 -7
package/cli/commands/record.ts
CHANGED
|
@@ -168,6 +168,7 @@ const RECORD_VALUE_FLAGS = new Set([
|
|
|
168
168
|
'--repo',
|
|
169
169
|
'--scene',
|
|
170
170
|
'--shell-url',
|
|
171
|
+
'--timeout',
|
|
171
172
|
])
|
|
172
173
|
const RECORD_SUBCOMMANDS = new Set([
|
|
173
174
|
'start',
|
|
@@ -924,8 +925,6 @@ async function recordStart(args: string[], opts: RecordOptions) {
|
|
|
924
925
|
peachExit(1)
|
|
925
926
|
}
|
|
926
927
|
} else {
|
|
927
|
-
await assertRecorderAvailable(bridge)
|
|
928
|
-
await assertTabVisibleForCapture(bridge)
|
|
929
928
|
const lockstep = args.includes('--lockstep')
|
|
930
929
|
if (lockstep && format !== 'mp4') {
|
|
931
930
|
console.error(' --lockstep requires --format mp4 (webm is realtime-clocked)')
|
|
@@ -942,7 +941,11 @@ async function recordStart(args: string[], opts: RecordOptions) {
|
|
|
942
941
|
writeRecordingState(session)
|
|
943
942
|
const result: { ok: boolean; error?: string } = await bridge.send({
|
|
944
943
|
type: 'evaluate',
|
|
945
|
-
code: `
|
|
944
|
+
code: `(() => {
|
|
945
|
+
if (!window.__sootsimRecorder) return { ok: false, error: 'recorder unavailable in this sim' };
|
|
946
|
+
if (document.hidden) return { ok: false, error: 'the peach tab is hidden; bring it to the foreground before recording' };
|
|
947
|
+
return window.__sootsimRecorder.start(${JSON.stringify(startOpts)});
|
|
948
|
+
})()`,
|
|
946
949
|
})
|
|
947
950
|
if (!result.ok) {
|
|
948
951
|
clearRecordingState()
|
|
@@ -1022,7 +1025,13 @@ async function recordPreludeStart(args: string[], opts: RecordOptions) {
|
|
|
1022
1025
|
// - the event recorder alone drives hidden PR-preview preludes before a
|
|
1023
1026
|
// visible live/combined segment exists
|
|
1024
1027
|
// returns null when the engine can't be reached.
|
|
1025
|
-
type EngineRecordingState =
|
|
1028
|
+
type EngineRecordingState =
|
|
1029
|
+
| 'idle'
|
|
1030
|
+
| 'starting'
|
|
1031
|
+
| 'recording'
|
|
1032
|
+
| 'processing'
|
|
1033
|
+
| 'ready'
|
|
1034
|
+
| 'prelude'
|
|
1026
1035
|
|
|
1027
1036
|
async function probeEngineRecordingState(
|
|
1028
1037
|
bridge: WsBridge,
|
|
@@ -1043,8 +1052,10 @@ async function probeEngineRecordingState(
|
|
|
1043
1052
|
})()`,
|
|
1044
1053
|
})
|
|
1045
1054
|
if (!snap) return null
|
|
1055
|
+
if (snap.headless === 'starting') return 'starting'
|
|
1046
1056
|
if (snap.store === 'recording' || snap.headless === 'recording') return 'recording'
|
|
1047
|
-
if (snap.store === 'processing') return 'processing'
|
|
1057
|
+
if (snap.store === 'processing' || snap.headless === 'processing') return 'processing'
|
|
1058
|
+
if (snap.headless === 'ready') return 'ready'
|
|
1048
1059
|
if (snap.eventRecording) return 'prelude'
|
|
1049
1060
|
if (snap.store === 'idle' || snap.headless === 'idle') return 'idle'
|
|
1050
1061
|
return null
|
|
@@ -1089,26 +1100,36 @@ async function cancelEngineRecording(bridge: WsBridge): Promise<{
|
|
|
1089
1100
|
|
|
1090
1101
|
async function recordStatus(args: string[], opts: RecordOptions) {
|
|
1091
1102
|
const state = readRecordingState()
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
)
|
|
1102
|
-
}
|
|
1103
|
-
return
|
|
1104
|
-
}
|
|
1105
|
-
// no `record start` session — but an atomic run or the rail button may
|
|
1106
|
-
// still be recording. ask the engine before claiming nothing is happening.
|
|
1107
|
-
const parsed = parseBridgeCliArgs(args, { port: opts.port, commandTimeoutMs: 8_000 })
|
|
1108
|
-
const bridge = createBridgeFromParsed(parsed)
|
|
1103
|
+
const parsed = parseBridgeCliArgs(args, {
|
|
1104
|
+
port: opts.port,
|
|
1105
|
+
commandTimeoutMs: 8_000,
|
|
1106
|
+
})
|
|
1107
|
+
if (state) noteSimMismatch(parsed.simId, state)
|
|
1108
|
+
const bridge = createBridgeFromParsed({
|
|
1109
|
+
...parsed,
|
|
1110
|
+
simId: state?.simId ?? parsed.simId,
|
|
1111
|
+
})
|
|
1109
1112
|
try {
|
|
1110
1113
|
const engineState = await probeEngineRecordingState(bridge)
|
|
1114
|
+
if (state) {
|
|
1115
|
+
if (engineState === 'idle') {
|
|
1116
|
+
clearRecordingState()
|
|
1117
|
+
console.log(' no recording in progress')
|
|
1118
|
+
} else if (engineState === 'ready') {
|
|
1119
|
+
console.log(' recording ready to save — run `peach record stop --output <path>`')
|
|
1120
|
+
} else if (engineState === null) {
|
|
1121
|
+
console.log(
|
|
1122
|
+
' recording state unavailable — the sim did not respond; the tracked session is preserved',
|
|
1123
|
+
)
|
|
1124
|
+
} else {
|
|
1125
|
+
console.log(
|
|
1126
|
+
` ${engineState} ${state.mode}${state.mode === 'video' ? ` (${state.format} @ ${state.fps}fps)` : ''} on sim ${state.simId ?? '?'} since ${state.startedAt}`,
|
|
1127
|
+
)
|
|
1128
|
+
}
|
|
1129
|
+
return
|
|
1130
|
+
}
|
|
1111
1131
|
if (
|
|
1132
|
+
engineState === 'starting' ||
|
|
1112
1133
|
engineState === 'recording' ||
|
|
1113
1134
|
engineState === 'processing' ||
|
|
1114
1135
|
engineState === 'prelude'
|
|
@@ -1154,8 +1175,10 @@ async function recordCancel(args: string[], opts: RecordOptions) {
|
|
|
1154
1175
|
try {
|
|
1155
1176
|
const engineState = await probeEngineRecordingState(probeBridge)
|
|
1156
1177
|
if (
|
|
1178
|
+
engineState === 'starting' ||
|
|
1157
1179
|
engineState === 'recording' ||
|
|
1158
1180
|
engineState === 'processing' ||
|
|
1181
|
+
engineState === 'ready' ||
|
|
1159
1182
|
engineState === 'prelude'
|
|
1160
1183
|
) {
|
|
1161
1184
|
// `record cancel` is explicit intent: the caller wants whatever is
|
|
@@ -1204,7 +1227,11 @@ async function recordCancel(args: string[], opts: RecordOptions) {
|
|
|
1204
1227
|
} else {
|
|
1205
1228
|
await bridge.send({
|
|
1206
1229
|
type: 'evaluate',
|
|
1207
|
-
code: `
|
|
1230
|
+
code: `(async () => {
|
|
1231
|
+
const recorder = window.__sootsimRecorder;
|
|
1232
|
+
await recorder.stop();
|
|
1233
|
+
return recorder.forceRelease();
|
|
1234
|
+
})()`,
|
|
1208
1235
|
})
|
|
1209
1236
|
}
|
|
1210
1237
|
} catch {
|
|
@@ -1241,7 +1268,15 @@ async function recordStop(args: string[], opts: RecordOptions) {
|
|
|
1241
1268
|
const bridge = createBridgeFromParsed({ ...parsed, simId })
|
|
1242
1269
|
try {
|
|
1243
1270
|
if (state.mode === 'live' || state.mode === 'combined') {
|
|
1244
|
-
|
|
1271
|
+
const stopped = await evalStoreStop(bridge)
|
|
1272
|
+
if (stopped === 'pending') {
|
|
1273
|
+
console.error(
|
|
1274
|
+
` stop failed: the ${state.mode} start is still in flight and has not begun a take yet.\n` +
|
|
1275
|
+
' run `peach record stop` again once it begins.',
|
|
1276
|
+
)
|
|
1277
|
+
peachExit(1)
|
|
1278
|
+
}
|
|
1279
|
+
if (!stopped) {
|
|
1245
1280
|
clearRecordingState()
|
|
1246
1281
|
console.error(
|
|
1247
1282
|
` stop failed: the ${state.mode} start still in flight never began a take, so there is nothing to upload.\n` +
|
|
@@ -1444,13 +1479,14 @@ async function evalStoreStart(
|
|
|
1444
1479
|
|
|
1445
1480
|
// the engine stop waits for a start still in flight. false means that start
|
|
1446
1481
|
// never began a take, so no upload will come and the store's last one belongs
|
|
1447
|
-
// to an earlier take.
|
|
1448
|
-
|
|
1449
|
-
|
|
1482
|
+
// to an earlier take. 'pending' means the start was still pending when the
|
|
1483
|
+
// engine stopped waiting, so it may yet begin one.
|
|
1484
|
+
async function evalStoreStop(bridge: WsBridge): Promise<boolean | 'pending'> {
|
|
1485
|
+
const stopped: boolean | 'pending' | null = await bridge.send({
|
|
1450
1486
|
type: 'evaluate',
|
|
1451
1487
|
code: `window.SootSim.bridges.stopRecording()`,
|
|
1452
1488
|
})
|
|
1453
|
-
return stopped !== false
|
|
1489
|
+
return stopped === 'pending' ? stopped : stopped !== false
|
|
1454
1490
|
}
|
|
1455
1491
|
|
|
1456
1492
|
interface StoreUploadResult {
|
package/cli/commands/swift.ts
CHANGED
|
@@ -165,7 +165,15 @@ export async function runSwift(
|
|
|
165
165
|
const appProject = resolveAppProject(packagePath)
|
|
166
166
|
host.configureSwiftStandinBehavior(packagePath, await loadRNXAppConfig(appProject))
|
|
167
167
|
// collection preflights every input path before any source bytes are read.
|
|
168
|
-
|
|
168
|
+
try {
|
|
169
|
+
host.swiftPackageWatchFiles(packagePath, undefined)
|
|
170
|
+
} catch (error) {
|
|
171
|
+
const { SwiftObjCCompilationError } = await import('../../src/swift-objc-compile')
|
|
172
|
+
if (!(error instanceof SwiftObjCCompilationError)) throw error
|
|
173
|
+
console.error(error.message)
|
|
174
|
+
host.disposeSwiftProject(packagePath)
|
|
175
|
+
return 1
|
|
176
|
+
}
|
|
169
177
|
const packageName = path.basename(packagePath)
|
|
170
178
|
const appInfo = host.swiftProjectAppInfo(packagePath)
|
|
171
179
|
const guestSource = swiftGuestBundle(packageName)
|
|
@@ -354,7 +362,7 @@ export async function runSwift(
|
|
|
354
362
|
await refresh()
|
|
355
363
|
controller.signal.throwIfAborted()
|
|
356
364
|
// refresh already printed the error
|
|
357
|
-
if (!metadata || compileError)
|
|
365
|
+
if (!metadata || compileError) return 1
|
|
358
366
|
if (opts.device) openArgs.push('--device', opts.device)
|
|
359
367
|
if (opts.driver) openArgs.push('--driver', opts.driver)
|
|
360
368
|
if (opts.headless) openArgs.push('--headless')
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
import { createHash } from 'node:crypto'
|
|
7
7
|
import { mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
8
8
|
import { dirname, extname, resolve } from 'node:path'
|
|
9
|
+
import {
|
|
10
|
+
collectTimelineAssetRefs,
|
|
11
|
+
timelineAssetRefOf,
|
|
12
|
+
} from 'peach-engine/screenshots/timeline-assets'
|
|
9
13
|
import {
|
|
10
14
|
isThreeModeSetting,
|
|
11
15
|
THREE_MODE_SETTING_IDS,
|
|
@@ -353,44 +357,6 @@ function mimeTypeForAsset(path: string, contentType: unknown) {
|
|
|
353
357
|
// timeline as blob: URLs.
|
|
354
358
|
const TIMELINE_ASSET_CHUNK_BYTES = 1_048_576
|
|
355
359
|
|
|
356
|
-
type TimelineAssetRef = {
|
|
357
|
-
src: string
|
|
358
|
-
recording: boolean
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function timelineAssetRefOf(value: Record<string, unknown>): TimelineAssetRef | null {
|
|
362
|
-
if (
|
|
363
|
-
(value.type === 'recording' ||
|
|
364
|
-
value.type === 'screenshot' ||
|
|
365
|
-
value.kind === 'screen') &&
|
|
366
|
-
typeof value.src === 'string' &&
|
|
367
|
-
!/^(?:blob:|data:|https?:)/i.test(value.src)
|
|
368
|
-
) {
|
|
369
|
-
return { src: value.src, recording: value.type === 'recording' }
|
|
370
|
-
}
|
|
371
|
-
return null
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function collectTimelineAssetRefs(value: unknown): TimelineAssetRef[] {
|
|
375
|
-
const refs: TimelineAssetRef[] = []
|
|
376
|
-
const seen = new Set<string>()
|
|
377
|
-
const visit = (node: unknown): void => {
|
|
378
|
-
if (Array.isArray(node)) {
|
|
379
|
-
for (const entry of node) visit(entry)
|
|
380
|
-
return
|
|
381
|
-
}
|
|
382
|
-
if (!isRecord(node)) return
|
|
383
|
-
for (const entry of Object.values(node)) visit(entry)
|
|
384
|
-
const ref = timelineAssetRefOf(node)
|
|
385
|
-
if (ref && !seen.has(ref.src)) {
|
|
386
|
-
seen.add(ref.src)
|
|
387
|
-
refs.push(ref)
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
visit(value)
|
|
391
|
-
return refs
|
|
392
|
-
}
|
|
393
|
-
|
|
394
360
|
type DistinctTimelineAsset = {
|
|
395
361
|
hash: string
|
|
396
362
|
mime: string
|
package/cli/commands/upload.ts
CHANGED
|
@@ -522,7 +522,7 @@ export async function runUpload(
|
|
|
522
522
|
bridge,
|
|
523
523
|
'(typeof window.__sootsimCaptureBundle === "function") ? window.__sootsimCaptureBundle() : null',
|
|
524
524
|
)
|
|
525
|
-
if (snapshot?.bundleUrl) {
|
|
525
|
+
if (hasBundle && snapshot?.bundleUrl) {
|
|
526
526
|
const bundleOrigin = new URL(snapshot.bundleUrl).origin
|
|
527
527
|
// manifest stays the body-less re-fetch path for bundle-origin static
|
|
528
528
|
// assets (chunks, fonts, wasm). bundle-origin api endpoints would also
|
|
@@ -784,7 +784,7 @@ export async function runUpload(
|
|
|
784
784
|
.join('\n')
|
|
785
785
|
const timelineGz = gzipSync(traceTimelineJsonl ? `${traceTimelineJsonl}\n` : '')
|
|
786
786
|
const storageGz =
|
|
787
|
-
opts.storageSnapshot && hasShareableStorage(opts.storageSnapshot)
|
|
787
|
+
hasBundle && opts.storageSnapshot && hasShareableStorage(opts.storageSnapshot)
|
|
788
788
|
? gzipSync(JSON.stringify(opts.storageSnapshot))
|
|
789
789
|
: undefined
|
|
790
790
|
console.log(
|
|
@@ -971,10 +971,9 @@ export async function runUpload(
|
|
|
971
971
|
// fetcher (same code path as the build's `peach app-fonts stage`) so both
|
|
972
972
|
// pipelines store identical bytes keyed by sha256(url).
|
|
973
973
|
const appFontFiles: PreparedFile[] = []
|
|
974
|
-
const appFontsWire =
|
|
975
|
-
appFontsWireFromPageHref(snapshot.pageHref),
|
|
976
|
-
|
|
977
|
-
)
|
|
974
|
+
const appFontsWire = hasBundle
|
|
975
|
+
? await resolveAppFontsWire(appFontsWireFromPageHref(snapshot.pageHref), fetch)
|
|
976
|
+
: ''
|
|
978
977
|
if (appFontsWire) {
|
|
979
978
|
const fonts = await fetchAppFontFiles(appFontsWire, {
|
|
980
979
|
onStaged: ({ url, byteLength }) =>
|
|
@@ -1003,7 +1002,7 @@ export async function runUpload(
|
|
|
1003
1002
|
// still-live metro and persist them keyed by sha256(url). the engine's
|
|
1004
1003
|
// <img src> resolves them through the share fetch proxy in preview mode.
|
|
1005
1004
|
const appSplashFiles: PreparedFile[] = []
|
|
1006
|
-
const appSplashWire = appSplashWireFromPageHref(snapshot.pageHref)
|
|
1005
|
+
const appSplashWire = hasBundle ? appSplashWireFromPageHref(snapshot.pageHref) : ''
|
|
1007
1006
|
if (appSplashWire) {
|
|
1008
1007
|
const images = await fetchAppSplashImages(appSplashWire, {
|
|
1009
1008
|
onStaged: ({ url, byteLength }) =>
|
|
@@ -1033,7 +1032,8 @@ export async function runUpload(
|
|
|
1033
1032
|
// explicitly so the installed app keeps its identity in share replay.
|
|
1034
1033
|
const appIdentityFiles: PreparedFile[] = []
|
|
1035
1034
|
let appManifest: string | undefined
|
|
1036
|
-
const metroOrigin =
|
|
1035
|
+
const metroOrigin =
|
|
1036
|
+
hasBundle && snapshot.bundleUrl ? loopbackOriginOf(snapshot.bundleUrl) : null
|
|
1037
1037
|
if (metroOrigin && snapshot.bundleUrl) {
|
|
1038
1038
|
const platform = new URL(snapshot.bundleUrl).searchParams.get('platform') || 'ios'
|
|
1039
1039
|
const manifestUrl = `${metroOrigin}/`
|
|
@@ -18,6 +18,7 @@ import { createServer } from 'net'
|
|
|
18
18
|
import { tmpdir } from 'os'
|
|
19
19
|
import { join } from 'path'
|
|
20
20
|
import { AUTOMATION_SIM_IDLE_REAP_TTL_MS } from '../../src/bridge-constants'
|
|
21
|
+
import { getCliVersion } from '../../src/cli-version'
|
|
21
22
|
import {
|
|
22
23
|
desktopHostDelivery,
|
|
23
24
|
desktopHostPlatform,
|
|
@@ -54,6 +55,38 @@ function probe(): DriverAvailability {
|
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
// every bridge since the fetch transport landed serves its route, so a 404
|
|
59
|
+
// from a server whose /healthz identifies a peach bridge means that bridge
|
|
60
|
+
// predates this cli. without that identity the port may hold something else,
|
|
61
|
+
// and the message says only what was observed. restarting closes every sim
|
|
62
|
+
// the bridge hosts, so the cli never does it.
|
|
63
|
+
async function olderBridgeMessage(bridgePort: number): Promise<string> {
|
|
64
|
+
const restart = `run \`peach daemon restart\` to load the matching bridge; it closes every sim that bridge hosts, so it was not restarted.`
|
|
65
|
+
let health: unknown = null
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(`http://127.0.0.1:${bridgePort}/healthz`, {
|
|
68
|
+
signal: AbortSignal.timeout(10_000),
|
|
69
|
+
})
|
|
70
|
+
health = await response.json()
|
|
71
|
+
} catch {
|
|
72
|
+
// an unreadable /healthz leaves the server unidentified
|
|
73
|
+
}
|
|
74
|
+
const field = (key: string) =>
|
|
75
|
+
health && typeof health === 'object' ? Reflect.get(health, key) : null
|
|
76
|
+
const pid = field('pid')
|
|
77
|
+
if (field('ok') !== true || typeof pid !== 'number') {
|
|
78
|
+
return `port ${bridgePort} has no fetch transport route and its /healthz did not identify a peach bridge, so this peach cli ${getCliVersion()} cannot use it. if an older peach bridge holds that port, ${restart}`
|
|
79
|
+
}
|
|
80
|
+
const runtime = field('activeRuntime')
|
|
81
|
+
const details = [
|
|
82
|
+
`pid ${pid}`,
|
|
83
|
+
typeof runtime === 'string' ? `runtime ${runtime}` : null,
|
|
84
|
+
]
|
|
85
|
+
.filter((part) => part !== null)
|
|
86
|
+
.join(', ')
|
|
87
|
+
return `the peach bridge on port ${bridgePort} (${details}) has no fetch transport route, so it predates this peach cli ${getCliVersion()}. ${restart}`
|
|
88
|
+
}
|
|
89
|
+
|
|
57
90
|
// resolve the Chrome user-data dir for a profiled launch. a non-empty
|
|
58
91
|
// return means the host should use a persistent context; '' means a plain
|
|
59
92
|
// (in-memory) browser launch.
|
|
@@ -111,6 +144,12 @@ async function launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult> {
|
|
|
111
144
|
`http://127.0.0.1:${opts.bridgePort}/__peach/fetch-transport`,
|
|
112
145
|
{ signal: AbortSignal.timeout(10_000) },
|
|
113
146
|
)
|
|
147
|
+
if (response.status === 404) {
|
|
148
|
+
return {
|
|
149
|
+
launched: false,
|
|
150
|
+
message: await olderBridgeMessage(opts.bridgePort),
|
|
151
|
+
}
|
|
152
|
+
}
|
|
114
153
|
if (!response.ok) throw new Error(`transport discovery returned ${response.status}`)
|
|
115
154
|
const descriptor: unknown = await response.json()
|
|
116
155
|
if (!descriptor || typeof descriptor !== 'object')
|
package/cli/flow-live-status.ts
CHANGED
|
@@ -13,8 +13,6 @@ export type LiveStepStatus = 'running' | 'success' | 'failure' | 'skipped'
|
|
|
13
13
|
|
|
14
14
|
type PlanStep = { index: number; name: string; target?: string }
|
|
15
15
|
|
|
16
|
-
const PAUSE_POLL_MS = 400
|
|
17
|
-
|
|
18
16
|
function replyPaused(reply: unknown) {
|
|
19
17
|
if (typeof reply !== 'object' || reply == null || !('control' in reply)) return false
|
|
20
18
|
const control = reply.control
|
package/cli/main.ts
CHANGED
|
@@ -466,7 +466,7 @@ try {
|
|
|
466
466
|
// inner verb's --json payload, so it never needs the shared bridge
|
|
467
467
|
// machinery that lives inside runInspect.
|
|
468
468
|
const { runAssert } = await import('./commands/assert')
|
|
469
|
-
await runAssert(parsed.commandArgs)
|
|
469
|
+
await runAssert(parsed.commandArgs, { port, sim: globalSimTarget })
|
|
470
470
|
break
|
|
471
471
|
}
|
|
472
472
|
|
|
@@ -38,7 +38,7 @@ export const OUTBOUND_ENDPOINTS: Record<string, OutboundEndpointDeclaration> = {
|
|
|
38
38
|
runtime_delivery: {
|
|
39
39
|
category: 'runtime-delivery',
|
|
40
40
|
destination:
|
|
41
|
-
'https://
|
|
41
|
+
'https://peachsim.com/runtimes/manifest.json and selected runtime tarball; with `peach desktop install --native`, https://r2.rnxsim.com/releases/native/<channel>/ manifest and versioned host zip',
|
|
42
42
|
preConsent: true,
|
|
43
43
|
payload: 'HTTP runtime or desktop host version request; no project or app data',
|
|
44
44
|
},
|
|
@@ -107,7 +107,7 @@ export const OUTBOUND_ENDPOINTS: Record<string, OutboundEndpointDeclaration> = {
|
|
|
107
107
|
},
|
|
108
108
|
release_notes: {
|
|
109
109
|
category: 'explicit-user-action',
|
|
110
|
-
destination: 'https://
|
|
110
|
+
destination: 'https://peachsim.com/api/changelog/sootsim',
|
|
111
111
|
preConsent: false,
|
|
112
112
|
payload: 'requested runtime version only',
|
|
113
113
|
},
|
|
@@ -639,6 +639,8 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
|
|
|
639
639
|
{ category: 'local_development', count: 1 },
|
|
640
640
|
'packages/peachsim/cli/drivers/playwright.ts :: fetch :: `http://127.0.0.1:${opts.bridgePort}/__peach/fetch-transport`':
|
|
641
641
|
{ category: 'local_development', count: 1 },
|
|
642
|
+
'packages/peachsim/cli/drivers/playwright.ts :: fetch :: `http://127.0.0.1:${bridgePort}/healthz`':
|
|
643
|
+
{ category: 'local_development', count: 1 },
|
|
642
644
|
"packages/peach-engine/src/screenshots/capture-store.ts :: fetch :: captureApiUrl(ref, 'grid')":
|
|
643
645
|
{ category: 'contrast_user_action', count: 1 },
|
|
644
646
|
'packages/peach-engine/src/screenshots/capture-store.ts :: fetch :: captureApiUrl(ref, variant)':
|
package/cli/ws-bridge.ts
CHANGED
|
@@ -374,12 +374,6 @@ export class BridgeSimLockedError extends Error {
|
|
|
374
374
|
}
|
|
375
375
|
}
|
|
376
376
|
|
|
377
|
-
interface WsResponse {
|
|
378
|
-
id: number
|
|
379
|
-
result?: any
|
|
380
|
-
error?: string
|
|
381
|
-
}
|
|
382
|
-
|
|
383
377
|
interface WsBridgeOptions {
|
|
384
378
|
connectTimeoutMs?: number
|
|
385
379
|
commandTimeoutMs?: number
|
package/detox/attempt-events.cjs
CHANGED
|
@@ -318,6 +318,10 @@ function writeDiscreteFrameReceipt(
|
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
function authenticateNativeCadenceCensus(root, sourceRoot, life, decoded) {
|
|
321
|
+
const {
|
|
322
|
+
authenticateNativeRefreshSamples,
|
|
323
|
+
} = require('../../peach-conformance/e2e/utils/native-frame-receipt.cjs')
|
|
324
|
+
authenticateNativeRefreshSamples(root, sourceRoot, life)
|
|
321
325
|
const native = life.nativeFramebuffer
|
|
322
326
|
if (
|
|
323
327
|
!native ||
|
|
@@ -344,13 +348,13 @@ function authenticateNativeCadenceCensus(root, sourceRoot, life, decoded) {
|
|
|
344
348
|
root,
|
|
345
349
|
native.mediaFrames,
|
|
346
350
|
native.mediaFramesSha256,
|
|
347
|
-
'native cadence
|
|
351
|
+
'native cadence sampled surface census',
|
|
348
352
|
)
|
|
349
353
|
.trim()
|
|
350
354
|
.split('\n')
|
|
351
355
|
.map(JSON.parse)
|
|
352
356
|
if (frames.length !== decoded.observations.length || media.length !== frames.length)
|
|
353
|
-
throw new Error('native cadence acquisition and
|
|
357
|
+
throw new Error('native cadence acquisition and sampled surface census differ')
|
|
354
358
|
const {
|
|
355
359
|
decodeNativeFramebuffer,
|
|
356
360
|
requireNativeRawFrame,
|
|
@@ -377,13 +381,25 @@ function authenticateNativeCadenceCensus(root, sourceRoot, life, decoded) {
|
|
|
377
381
|
return row.callbackIndex
|
|
378
382
|
}),
|
|
379
383
|
)
|
|
380
|
-
const
|
|
384
|
+
const reads = boundText(
|
|
385
|
+
root,
|
|
386
|
+
native.reads,
|
|
387
|
+
native.readsSha256,
|
|
388
|
+
'native cadence observations',
|
|
389
|
+
)
|
|
390
|
+
.split('\n')
|
|
391
|
+
.filter(Boolean)
|
|
392
|
+
.map(JSON.parse)
|
|
381
393
|
let expectedCallback = 0
|
|
382
|
-
for (const
|
|
394
|
+
for (const read of reads) {
|
|
383
395
|
while (superseded.delete(expectedCallback)) expectedCallback++
|
|
396
|
+
if (read.callbackIndex !== expectedCallback++)
|
|
397
|
+
throw new Error('native cadence observations have invalid callback order')
|
|
398
|
+
}
|
|
399
|
+
const origin = frames[0].consumerArrival
|
|
400
|
+
for (const [index, frame] of frames.entries()) {
|
|
384
401
|
const clock = frame.consumerArrival
|
|
385
402
|
if (
|
|
386
|
-
frame.callbackIndex !== expectedCallback++ ||
|
|
387
403
|
media[index].sourceFrameIndex !== index ||
|
|
388
404
|
!clock ||
|
|
389
405
|
clock.domain !== 'host-mach-absolute-time' ||
|
|
@@ -771,6 +787,26 @@ function discreteFrameEvidenceFiles(root, evidence) {
|
|
|
771
787
|
paths.add(native.receipt)
|
|
772
788
|
paths.add(native.superseded)
|
|
773
789
|
paths.add(native.mediaFrames)
|
|
790
|
+
paths.add(native.gradingMarkers)
|
|
791
|
+
paths.add(native.gradingPlan)
|
|
792
|
+
paths.add(native.windowResolution)
|
|
793
|
+
paths.add(native.reads)
|
|
794
|
+
for (const receipt of native.finalReads) {
|
|
795
|
+
const text = boundText(
|
|
796
|
+
root,
|
|
797
|
+
receipt.path,
|
|
798
|
+
receipt.sha256,
|
|
799
|
+
'native sampled end state',
|
|
800
|
+
)
|
|
801
|
+
paths.add(receipt.path)
|
|
802
|
+
for (const row of text.split('\n').filter(Boolean).map(JSON.parse)) {
|
|
803
|
+
const file = proofBoundPath(
|
|
804
|
+
path.dirname(proofBoundPath(root, receipt.path)),
|
|
805
|
+
row.rawPath,
|
|
806
|
+
)
|
|
807
|
+
paths.add(path.relative(root, file).split(path.sep).join('/'))
|
|
808
|
+
}
|
|
809
|
+
}
|
|
774
810
|
const rows = fs
|
|
775
811
|
.readFileSync(proofBoundPath(root, native.receipt), 'utf8')
|
|
776
812
|
.trim()
|