peachsim 0.1.1625 → 0.1.1640
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/LICENSE +1 -1
- package/README.md +15 -2
- package/cli/app-config.ts +18 -6
- package/cli/app-fonts.ts +37 -17
- package/cli/app-stubs.ts +284 -20
- package/cli/bridge-flow-runner.ts +66 -45
- package/cli/cloud-client.ts +60 -40
- package/cli/command-registry.ts +1 -0
- package/cli/commands/control.ts +283 -49
- package/cli/commands/doctor.ts +30 -189
- package/cli/commands/flow.ts +79 -11
- package/cli/commands/inspect/core.ts +10 -5
- package/cli/commands/inspect/settling.ts +14 -2
- package/cli/commands/inspect/shared.ts +9 -4
- package/cli/commands/inspect/wait-ready.ts +12 -0
- package/cli/commands/inspect.ts +91 -50
- package/cli/commands/install-desktop.ts +1 -0
- package/cli/commands/maestro-compare.ts +97 -38
- package/cli/commands/maestro.ts +9 -1
- package/cli/commands/preview.ts +1 -1
- package/cli/commands/record.ts +118 -48
- package/cli/commands/screenshot.ts +23 -1
- package/cli/commands/serve.ts +7 -2
- package/cli/commands/setup.ts +19 -42
- package/cli/commands/swift.ts +14 -3
- package/cli/commands/upload.ts +93 -48
- package/cli/desktop-companion.ts +102 -17
- package/cli/drivers/desktop.ts +14 -1
- package/cli/drivers/playwright-sim-host.ts +103 -45
- package/cli/drivers/playwright.ts +9 -8
- package/cli/drivers/types.ts +5 -0
- package/cli/maestro-accessibility.ts +1 -1
- package/cli/maestro-compare.ts +84 -12
- package/cli/main.ts +3 -2
- package/cli/outbound-endpoints.ts +20 -6
- package/cli/pause-aware-timeout.ts +106 -0
- package/cli/scene-video.ts +89 -0
- package/cli/ws-bridge.ts +37 -19
- package/detox/attempt-events.cjs +66 -5
- package/detox/attempt-events.d.cts +7 -0
- package/detox/expectations.ts +7 -3
- package/detox/index.ts +269 -170
- package/detox/motion-change.cjs +206 -21
- package/detox/motion-change.d.cts +39 -2
- package/detox/navigation-lifecycle.ts +35 -3
- 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 +8 -7
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +13 -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 +14 -8
- package/dist-lib/bridge-contract.mjs +13 -7
- package/dist-lib/bundle-channel.cjs +11887 -7900
- 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 +6 -3
- package/dist-lib/config.mjs +6 -3
- package/dist-lib/detox/index.cjs +1379 -725
- package/dist-lib/dev-bundle-resolution.cjs +14 -2
- package/dist-lib/expo-swift-bundle.cjs +9341 -5462
- package/dist-lib/expo-swift-metadata.cjs +11 -7
- package/dist-lib/expo-swift-metadata.mjs +11 -7
- package/dist-lib/home-paths.cjs +32 -3
- package/dist-lib/host/bridge-host.cjs +50204 -7258
- 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 +3541 -448
- 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 +1 -1
- package/dist-lib/menu.mjs +1 -1
- 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 +2931 -104
- package/dist-lib/native-app-identity-project.cjs +289 -49
- package/dist-lib/native-app-identity-project.mjs +289 -49
- package/dist-lib/native-app-identity.cjs +77 -26
- package/dist-lib/native-app-identity.mjs +76 -26
- 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 +6 -2
- package/dist-lib/react-native-host-modules.mjs +6 -2
- package/dist-lib/scripts/dev-server-scanner.cjs +22 -8
- package/dist-lib/sdk.cjs +27 -11
- package/dist-lib/sdk.mjs +27 -10
- package/dist-lib/skills.cjs +72201 -26208
- package/dist-lib/swift-compile.cjs +1007 -140
- package/dist-lib/swift-compile.mjs +1049 -174
- package/dist-lib/swift-package.cjs +12 -3
- package/dist-lib/swift-package.mjs +12 -3
- package/dist-lib/swift-submit.cjs +13 -5
- package/dist-lib/swift-submit.mjs +11 -5
- package/dist-lib/swift-toolchain.cjs +423 -33
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite-swift.cjs +8628 -5268
- package/dist-lib/vite.cjs +2859 -12
- package/dist-lib/worklet-executable.cjs +14 -2
- package/dist-lib/worklet-executable.mjs +14 -2
- package/kotlin/AppSources/dev/peach/app/PeachDrawable.kt +21 -14
- package/kotlin/Sources/androidx/annotation/DrawableRes.kt +14 -0
- package/package.json +1 -1
- package/scripts/dev-server-scanner.ts +2 -1
- package/skills/peach-perf/SKILL.md +11 -5
- package/skills/peach-setup/SKILL.md +16 -19
- package/src/backend-origin.ts +13 -6
- package/src/bridge-constants.ts +11 -0
- package/src/bridge-contract.ts +22 -34
- package/src/config.ts +15 -0
- package/src/desktop-host-delivery.ts +5 -1
- package/src/dev-bundle-resolution.ts +2 -1
- package/src/expo-manifest-body.ts +13 -0
- package/src/expo-swift-host.ts +196 -49
- package/src/expo-swift-ready.ts +78 -0
- package/src/expoKotlinContract.ts +4 -7
- package/src/expoSwiftContract.ts +17 -6
- package/src/home-paths.ts +48 -2
- package/src/host/bridge-host.ts +5 -2
- package/src/host/staged-host-files.ts +9 -0
- package/src/native-app-identity-project.ts +27 -6
- package/src/native-app-identity.ts +91 -4
- package/src/native-firebase-config.ts +209 -13
- package/src/nativeAppIconContract.ts +1 -4
- package/src/output-lock-stdio.ts +36 -0
- package/src/process-group.ts +25 -2
- package/src/react-native-bridge-modules.ts +431 -0
- package/src/react-native-host-modules.ts +8 -1
- package/src/settings-bundle.ts +1 -4
- package/src/swift-binary-modules.ts +2 -2
- package/src/swift-compile.ts +918 -109
- package/src/swift-compiler-digest.ts +14 -0
- package/src/swift-distribution.lock.json +24 -12
- package/src/swift-distribution.ts +103 -4
- package/src/swift-host.ts +180 -55
- package/src/swift-icu.ts +27 -13
- package/src/swift-objc-compile.ts +355 -0
- package/src/swift-objc-sdk.ts +821 -27
- package/src/swift-objc-standin.ts +148 -14
- package/src/swift-package-manifest.ts +60 -7
- package/src/swift-package.ts +15 -1
- package/src/swift-packages.ts +154 -81
- package/src/swift-standin-emit.ts +96 -15
- package/src/swift-standins.ts +530 -116
- package/src/swift-submit.ts +13 -5
- package/src/swift-toolchain.ts +500 -42
- package/src/vite-plugin.ts +12 -1
- package/src/worker-system-chunks.ts +11 -12
- package/src/worklet-executable.ts +19 -1
- package/src/xcode-project.ts +270 -30
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
|
|
6
|
+
// the team machine load governor SIGSTOPs a job and records its pids in this
|
|
7
|
+
// ledger (`{ [rootPid]: [{ pid, started_at_ms }] }`, `{}` when nothing is
|
|
8
|
+
// held) until it resumes them. a plain setTimeout keeps running across that
|
|
9
|
+
// hold, so a command the paused browser could not answer times out for load
|
|
10
|
+
// it never caused. a deadline only stops advancing while the pid it waits on
|
|
11
|
+
// is held; a dead or unrelated process never extends it.
|
|
12
|
+
const LEDGER_PATH = path.join(
|
|
13
|
+
process.env.TM_HOME || path.join(os.homedir(), '.team-machine'),
|
|
14
|
+
'governor-pauses.json',
|
|
15
|
+
)
|
|
16
|
+
const TICK_MS = 500
|
|
17
|
+
|
|
18
|
+
interface Timer {
|
|
19
|
+
hostPid: number
|
|
20
|
+
remaining: number
|
|
21
|
+
accountedAt: number
|
|
22
|
+
fire: () => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const timers = new Set<Timer>()
|
|
26
|
+
let poll: ReturnType<typeof setInterval> | undefined
|
|
27
|
+
|
|
28
|
+
function pausedPids(): number[] {
|
|
29
|
+
try {
|
|
30
|
+
const ledger: Record<string, { pid: number }[]> = JSON.parse(
|
|
31
|
+
readFileSync(LEDGER_PATH, 'utf8'),
|
|
32
|
+
)
|
|
33
|
+
return Object.values(ledger).flatMap((procs) => procs.map((proc) => proc.pid))
|
|
34
|
+
} catch {
|
|
35
|
+
return []
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// the pids in `paused` that are `rootPid` or descend from it, by ps ancestry.
|
|
40
|
+
function heldRoots(paused: number[], roots: Set<number>): Set<number> {
|
|
41
|
+
const parents = new Map<number, number>()
|
|
42
|
+
for (const line of execFileSync('ps', ['-A', '-o', 'pid=,ppid='], {
|
|
43
|
+
encoding: 'utf8',
|
|
44
|
+
}).split('\n')) {
|
|
45
|
+
const [pid, ppid] = line.trim().split(/\s+/).map(Number)
|
|
46
|
+
if (pid && ppid !== undefined) parents.set(pid, ppid)
|
|
47
|
+
}
|
|
48
|
+
const held = new Set<number>()
|
|
49
|
+
for (const start of paused) {
|
|
50
|
+
for (let pid: number | undefined = start, hops = 0; pid && hops < 64; hops++) {
|
|
51
|
+
if (roots.has(pid)) {
|
|
52
|
+
held.add(pid)
|
|
53
|
+
break
|
|
54
|
+
}
|
|
55
|
+
pid = parents.get(pid)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return held
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function tick() {
|
|
62
|
+
const now = Date.now()
|
|
63
|
+
const paused = pausedPids()
|
|
64
|
+
const held = paused.length
|
|
65
|
+
? heldRoots(paused, new Set([...timers].map((timer) => timer.hostPid)))
|
|
66
|
+
: new Set<number>()
|
|
67
|
+
for (const timer of [...timers]) {
|
|
68
|
+
// each timer accounts only since its own creation or last accounting
|
|
69
|
+
if (!held.has(timer.hostPid)) timer.remaining -= now - timer.accountedAt
|
|
70
|
+
timer.accountedAt = now
|
|
71
|
+
if (timer.remaining <= 0) {
|
|
72
|
+
timers.delete(timer)
|
|
73
|
+
timer.fire()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (timers.size === 0 && poll) {
|
|
77
|
+
clearInterval(poll)
|
|
78
|
+
poll = undefined
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// like setTimeout, but the budget does not advance while the governor holds
|
|
83
|
+
// `hostPid` (the browser host the command waits on) or a descendant paused.
|
|
84
|
+
// one shared poll serves every timer. returns the clear function.
|
|
85
|
+
export function setPauseAwareTimeout(
|
|
86
|
+
fn: () => void,
|
|
87
|
+
budgetMs: number,
|
|
88
|
+
hostPid: number | undefined,
|
|
89
|
+
): () => void {
|
|
90
|
+
if (hostPid === undefined) {
|
|
91
|
+
const timer = setTimeout(fn, budgetMs)
|
|
92
|
+
return () => clearTimeout(timer)
|
|
93
|
+
}
|
|
94
|
+
const timer: Timer = { hostPid, remaining: budgetMs, accountedAt: Date.now(), fire: fn }
|
|
95
|
+
timers.add(timer)
|
|
96
|
+
if (!poll) {
|
|
97
|
+
poll = setInterval(tick, TICK_MS)
|
|
98
|
+
}
|
|
99
|
+
return () => {
|
|
100
|
+
timers.delete(timer)
|
|
101
|
+
if (timers.size === 0 && poll) {
|
|
102
|
+
clearInterval(poll)
|
|
103
|
+
poll = undefined
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises'
|
|
2
|
+
import { createRequire } from 'node:module'
|
|
3
|
+
import { readSceneRecordingUpload } from 'peach-engine/remote-stream/scene-recording'
|
|
4
|
+
import { launchReapedChrome } from '../../../scripts/lib/reap-browser'
|
|
5
|
+
import {
|
|
6
|
+
ensurePlaywrightBrowser,
|
|
7
|
+
provisionPlaywright,
|
|
8
|
+
} from './drivers/playwright-provisioning'
|
|
9
|
+
|
|
10
|
+
// render the saved display stream in an isolated viewer. guest code never runs,
|
|
11
|
+
// and capture time is independent of the machine that recorded the app.
|
|
12
|
+
export async function renderSceneToVideo(options: {
|
|
13
|
+
scene: Uint8Array
|
|
14
|
+
shellUrl: string
|
|
15
|
+
output: string
|
|
16
|
+
}): Promise<{
|
|
17
|
+
durationMs: number
|
|
18
|
+
frameCount: number
|
|
19
|
+
renderTimeMs: number
|
|
20
|
+
sizeBytes: number
|
|
21
|
+
}> {
|
|
22
|
+
const { header } = await readSceneRecordingUpload(options.scene)
|
|
23
|
+
const resolved = await provisionPlaywright()
|
|
24
|
+
const installed = await ensurePlaywrightBrowser(resolved, true)
|
|
25
|
+
if (!installed.ok) throw new Error(installed.message)
|
|
26
|
+
const require = createRequire(import.meta.url)
|
|
27
|
+
const { chromium }: typeof import('playwright-core') = require(resolved.modulePath)
|
|
28
|
+
const browser = await launchReapedChrome((opts) => chromium.launch(opts), {
|
|
29
|
+
headless: true,
|
|
30
|
+
...(installed.libraryPath
|
|
31
|
+
? { env: { ...process.env, LD_LIBRARY_PATH: installed.libraryPath } }
|
|
32
|
+
: {}),
|
|
33
|
+
})
|
|
34
|
+
try {
|
|
35
|
+
const source = new URL(options.shellUrl)
|
|
36
|
+
const url = new URL(source.pathname, source.origin)
|
|
37
|
+
url.searchParams.set('device', header.peer.deviceProfile)
|
|
38
|
+
url.searchParams.set('colorScheme', header.peer.appearance)
|
|
39
|
+
for (const key of ['orientation']) {
|
|
40
|
+
const value = source.searchParams.get(key)
|
|
41
|
+
if (value) url.searchParams.set(key, value)
|
|
42
|
+
}
|
|
43
|
+
const sceneUrl = new URL('__scene_export.psr.gz', url).href
|
|
44
|
+
const page = await browser.newPage({
|
|
45
|
+
viewport: { width: 600, height: 1000 },
|
|
46
|
+
deviceScaleFactor: 3,
|
|
47
|
+
})
|
|
48
|
+
await page.route(sceneUrl, (route) =>
|
|
49
|
+
route.fulfill({
|
|
50
|
+
contentType: 'application/vnd.peach.scene-stream+gzip',
|
|
51
|
+
body: Buffer.from(options.scene),
|
|
52
|
+
}),
|
|
53
|
+
)
|
|
54
|
+
url.searchParams.set(
|
|
55
|
+
'remoteEngine',
|
|
56
|
+
JSON.stringify({ url: sceneUrl, expectedPeer: header.peer, autoplay: false }),
|
|
57
|
+
)
|
|
58
|
+
await page.goto(url.href)
|
|
59
|
+
await page
|
|
60
|
+
.locator('[data-sootsim-boot-splash="hidden"]')
|
|
61
|
+
.waitFor({ state: 'attached', timeout: 120_000 })
|
|
62
|
+
await page.waitForFunction(
|
|
63
|
+
() => !!window.SootSim?.bridges.recorder && !!window.SootSim.bridges.sceneStream,
|
|
64
|
+
undefined,
|
|
65
|
+
{ timeout: 120_000 },
|
|
66
|
+
)
|
|
67
|
+
const result = await page.evaluate(async () => {
|
|
68
|
+
const recorder = window.SootSim?.bridges.recorder
|
|
69
|
+
if (!recorder) throw new Error('scene recorder bridge unavailable')
|
|
70
|
+
return recorder.renderScene()
|
|
71
|
+
})
|
|
72
|
+
const chunks: Buffer[] = []
|
|
73
|
+
let offset = 0
|
|
74
|
+
for (;;) {
|
|
75
|
+
const chunk = await page.evaluate(async (at) => {
|
|
76
|
+
return window.SootSim?.bridges.recorder?.getBlobBase64({ offset: at })
|
|
77
|
+
}, offset)
|
|
78
|
+
if (!chunk) throw new Error('scene render returned no video')
|
|
79
|
+
chunks.push(Buffer.from(chunk.data, 'base64'))
|
|
80
|
+
offset = chunk.offset
|
|
81
|
+
if (chunk.done) break
|
|
82
|
+
}
|
|
83
|
+
const video = Buffer.concat(chunks)
|
|
84
|
+
await writeFile(options.output, video)
|
|
85
|
+
return { ...result, sizeBytes: video.byteLength }
|
|
86
|
+
} finally {
|
|
87
|
+
await browser.close()
|
|
88
|
+
}
|
|
89
|
+
}
|
package/cli/ws-bridge.ts
CHANGED
|
@@ -10,6 +10,7 @@ import fs from 'node:fs'
|
|
|
10
10
|
import path from 'node:path'
|
|
11
11
|
import { WebSocket } from 'ws'
|
|
12
12
|
import {
|
|
13
|
+
assertPeachPortEnvironment,
|
|
13
14
|
DEFAULT_SOOTSIM_BRIDGE_PORT,
|
|
14
15
|
resolveSootsimBridgePort,
|
|
15
16
|
} from '../src/bridge-constants'
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
readCurrentSimId,
|
|
32
33
|
saveCurrentSimId,
|
|
33
34
|
} from './current-sim'
|
|
35
|
+
import { setPauseAwareTimeout } from './pause-aware-timeout'
|
|
34
36
|
import type { WsBridgeCommand } from '../src/bridge-contract'
|
|
35
37
|
|
|
36
38
|
// --- bridge world resolution ---------------------------------------------
|
|
@@ -297,6 +299,7 @@ export function resolveBridgePortForPin(pin: {
|
|
|
297
299
|
* vs repo checkout) — invocation-path heuristics made `peach list` flap
|
|
298
300
|
* between worlds. */
|
|
299
301
|
export function resolveDefaultBridgePort(): number {
|
|
302
|
+
assertPeachPortEnvironment(process.env)
|
|
300
303
|
return resolveBridgeWorld().bridgePort
|
|
301
304
|
}
|
|
302
305
|
|
|
@@ -330,9 +333,11 @@ export function printBridgeWorldNotice(
|
|
|
330
333
|
export interface WsBridge {
|
|
331
334
|
readonly plane?: 'local' | 'cloud'
|
|
332
335
|
// `opts.timeoutMs` overrides the bridge-wide commandTimeoutMs for a single
|
|
333
|
-
// call
|
|
334
|
-
|
|
335
|
-
|
|
336
|
+
// call. defaultTimeoutMs supplies a probe cap unless --timeout was explicit.
|
|
337
|
+
send(
|
|
338
|
+
cmd: WsBridgeCommand,
|
|
339
|
+
opts?: { timeoutMs?: number; defaultTimeoutMs?: number },
|
|
340
|
+
): Promise<any>
|
|
336
341
|
// includeRetained appends the bridge's retained disconnected sims
|
|
337
342
|
// (readyState 'closed' with disconnectCause) after the live ones. only
|
|
338
343
|
// loss-reporting surfaces pass it; every other consumer keeps the
|
|
@@ -378,6 +383,7 @@ interface WsResponse {
|
|
|
378
383
|
interface WsBridgeOptions {
|
|
379
384
|
connectTimeoutMs?: number
|
|
380
385
|
commandTimeoutMs?: number
|
|
386
|
+
explicitTimeout?: boolean
|
|
381
387
|
simId?: string
|
|
382
388
|
cliLabel?: string
|
|
383
389
|
leaseScope?: 'connection'
|
|
@@ -452,12 +458,14 @@ export interface ParsedBridgeCliArgs {
|
|
|
452
458
|
simId?: string
|
|
453
459
|
simIdSource: 'flag' | 'saved' | 'none'
|
|
454
460
|
commandTimeoutMs: number
|
|
461
|
+
explicitTimeout?: boolean
|
|
455
462
|
}
|
|
456
463
|
|
|
457
464
|
interface BridgeFlagState {
|
|
458
465
|
wsPort: number
|
|
459
466
|
explicitPort: boolean
|
|
460
467
|
commandTimeoutMs: number
|
|
468
|
+
explicitTimeout?: boolean
|
|
461
469
|
simId: string | undefined
|
|
462
470
|
simIdSource: ParsedBridgeCliArgs['simIdSource']
|
|
463
471
|
}
|
|
@@ -484,7 +492,10 @@ const BRIDGE_VALUE_FLAGS_TABLE: readonly {
|
|
|
484
492
|
{
|
|
485
493
|
names: ['--timeout'],
|
|
486
494
|
apply: (state, value) => {
|
|
487
|
-
if (value !== undefined)
|
|
495
|
+
if (value !== undefined) {
|
|
496
|
+
state.commandTimeoutMs = Number(value)
|
|
497
|
+
state.explicitTimeout = true
|
|
498
|
+
}
|
|
488
499
|
},
|
|
489
500
|
},
|
|
490
501
|
{
|
|
@@ -570,10 +581,12 @@ export function parseBridgeCliArgs(
|
|
|
570
581
|
simId: state.simId,
|
|
571
582
|
simIdSource: state.simIdSource,
|
|
572
583
|
commandTimeoutMs: state.commandTimeoutMs,
|
|
584
|
+
explicitTimeout: state.explicitTimeout,
|
|
573
585
|
}
|
|
574
586
|
}
|
|
575
587
|
|
|
576
588
|
export function createBridge(wsPort: number, opts: WsBridgeOptions = {}): WsBridge {
|
|
589
|
+
assertPeachPortEnvironment(process.env)
|
|
577
590
|
let nextId = 1
|
|
578
591
|
const commandTimeoutMs = opts.commandTimeoutMs ?? 15000
|
|
579
592
|
const pending = new Map<
|
|
@@ -745,7 +758,10 @@ export function createBridge(wsPort: number, opts: WsBridgeOptions = {}): WsBrid
|
|
|
745
758
|
async send(cmd, callOpts) {
|
|
746
759
|
// NOTE: `opts` (the createBridge closure arg) carries simId; do not
|
|
747
760
|
// shadow it with the per-call arg.
|
|
748
|
-
const effectiveTimeoutMs =
|
|
761
|
+
const effectiveTimeoutMs =
|
|
762
|
+
callOpts?.timeoutMs ??
|
|
763
|
+
(opts.explicitTimeout ? commandTimeoutMs : callOpts?.defaultTimeoutMs) ??
|
|
764
|
+
commandTimeoutMs
|
|
749
765
|
const sendOnce = async (
|
|
750
766
|
defaultSimId?: string,
|
|
751
767
|
cmdOverride?: Record<string, any>,
|
|
@@ -754,30 +770,31 @@ export function createBridge(wsPort: number, opts: WsBridgeOptions = {}): WsBrid
|
|
|
754
770
|
if (terminalError) throw terminalError
|
|
755
771
|
const id = nextId++
|
|
756
772
|
return new Promise((resolve, reject) => {
|
|
757
|
-
const
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
)
|
|
764
|
-
|
|
773
|
+
const payload = { ...(cmdOverride ?? cmd), id } as Record<string, any>
|
|
774
|
+
if (payload.simId === undefined && defaultSimId) {
|
|
775
|
+
payload.simId = defaultSimId
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const clearTimeoutFn = setPauseAwareTimeout(
|
|
779
|
+
() => {
|
|
780
|
+
pending.delete(id)
|
|
781
|
+
reject(new Error(`command timed out after ${effectiveTimeoutMs / 1000}s`))
|
|
782
|
+
},
|
|
783
|
+
effectiveTimeoutMs,
|
|
784
|
+
lineageBySimId.get(payload.simId)?.hostPid,
|
|
785
|
+
)
|
|
765
786
|
|
|
766
787
|
pending.set(id, {
|
|
767
788
|
resolve: (v) => {
|
|
768
|
-
|
|
789
|
+
clearTimeoutFn()
|
|
769
790
|
resolve(v)
|
|
770
791
|
},
|
|
771
792
|
reject: (e) => {
|
|
772
|
-
|
|
793
|
+
clearTimeoutFn()
|
|
773
794
|
reject(e)
|
|
774
795
|
},
|
|
775
796
|
})
|
|
776
797
|
|
|
777
|
-
const payload = { ...(cmdOverride ?? cmd), id } as Record<string, any>
|
|
778
|
-
if (payload.simId === undefined && defaultSimId) {
|
|
779
|
-
payload.simId = defaultSimId
|
|
780
|
-
}
|
|
781
798
|
maybeNoticeTarget(payload.type ?? '', payload.simId)
|
|
782
799
|
ws.send(JSON.stringify(payload))
|
|
783
800
|
})
|
|
@@ -946,6 +963,7 @@ export function createBridgeFromParsed(parsed: ParsedBridgeCliArgs): WsBridge {
|
|
|
946
963
|
if (cloudBridge) return cloudBridge
|
|
947
964
|
const options = {
|
|
948
965
|
commandTimeoutMs: parsed.commandTimeoutMs,
|
|
966
|
+
explicitTimeout: parsed.explicitTimeout,
|
|
949
967
|
simId: parsed.simId,
|
|
950
968
|
simIdSource: parsed.simIdSource,
|
|
951
969
|
}
|
package/detox/attempt-events.cjs
CHANGED
|
@@ -43,6 +43,15 @@ function proofBoundPath(root, relative) {
|
|
|
43
43
|
return resolved
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// lifecycle receipts bind every clock and census read to the retained bytes.
|
|
47
|
+
function boundText(root, relative, sha256, label) {
|
|
48
|
+
const bytes = fs.readFileSync(proofBoundPath(root, relative))
|
|
49
|
+
if (proofByteHash(bytes) !== sha256)
|
|
50
|
+
throw new Error(`${label} does not match its recorded hash`)
|
|
51
|
+
return bytes.toString('utf8')
|
|
52
|
+
}
|
|
53
|
+
module.exports.boundText = boundText
|
|
54
|
+
|
|
46
55
|
function authenticateDiscreteFrameAsset(root, declaration, calibrate = false) {
|
|
47
56
|
const cadence = parseDiscreteFrameCadence(declaration)
|
|
48
57
|
const fixture = fs.readFileSync(proofBoundPath(root, cadence.asset.fixturePath))
|
|
@@ -125,6 +134,10 @@ function beginDiscreteFrameCapture({
|
|
|
125
134
|
}) {
|
|
126
135
|
if (target.startsWith('android'))
|
|
127
136
|
throw new Error('discrete frame cadence is uncalibrated on Android')
|
|
137
|
+
if (!fs.existsSync(path.join(root, 'capture-context.json')))
|
|
138
|
+
throw new Error(
|
|
139
|
+
`discrete frame cadence needs an attempt minted by the paired proof capture (bun run test:conformance:proof); ${root} has no capture-context.json`,
|
|
140
|
+
)
|
|
128
141
|
const attempt = JSON.parse(
|
|
129
142
|
fs.readFileSync(path.join(root, 'capture-context.json'), 'utf8'),
|
|
130
143
|
)
|
|
@@ -318,13 +331,21 @@ function authenticateNativeCadenceCensus(root, sourceRoot, life, decoded) {
|
|
|
318
331
|
)
|
|
319
332
|
)
|
|
320
333
|
throw new Error('native cadence has no source-matched framebuffer consumer')
|
|
321
|
-
const frames =
|
|
322
|
-
|
|
334
|
+
const frames = boundText(
|
|
335
|
+
root,
|
|
336
|
+
native.receipt,
|
|
337
|
+
native.receiptSha256,
|
|
338
|
+
'native cadence source receipt',
|
|
339
|
+
)
|
|
323
340
|
.trim()
|
|
324
341
|
.split('\n')
|
|
325
342
|
.map(JSON.parse)
|
|
326
|
-
const media =
|
|
327
|
-
|
|
343
|
+
const media = boundText(
|
|
344
|
+
root,
|
|
345
|
+
native.mediaFrames,
|
|
346
|
+
native.mediaFramesSha256,
|
|
347
|
+
'native cadence presentation census',
|
|
348
|
+
)
|
|
328
349
|
.trim()
|
|
329
350
|
.split('\n')
|
|
330
351
|
.map(JSON.parse)
|
|
@@ -335,11 +356,34 @@ function authenticateNativeCadenceCensus(root, sourceRoot, life, decoded) {
|
|
|
335
356
|
requireNativeRawFrame,
|
|
336
357
|
} = require('../../peach-conformance/e2e/utils/native-frame-receipt.cjs')
|
|
337
358
|
const rawRoot = path.dirname(proofBoundPath(root, native.receipt))
|
|
359
|
+
// a callback whose frame a newer one overwrote mid-read keeps no bytes; its
|
|
360
|
+
// superseded receipt fills exactly the gap it leaves in the callback order
|
|
361
|
+
const superseded = new Set(
|
|
362
|
+
boundText(
|
|
363
|
+
root,
|
|
364
|
+
native.superseded,
|
|
365
|
+
native.supersededSha256,
|
|
366
|
+
'native cadence superseded receipt',
|
|
367
|
+
)
|
|
368
|
+
.split('\n')
|
|
369
|
+
.filter(Boolean)
|
|
370
|
+
.map((line) => {
|
|
371
|
+
const row = JSON.parse(line)
|
|
372
|
+
if (
|
|
373
|
+
!Number.isSafeInteger(row.callbackIndex) ||
|
|
374
|
+
row.seedBeforeRead === row.seedAfterRead
|
|
375
|
+
)
|
|
376
|
+
throw new Error('native superseded receipt is not an overwritten read')
|
|
377
|
+
return row.callbackIndex
|
|
378
|
+
}),
|
|
379
|
+
)
|
|
338
380
|
const origin = frames[0].consumerArrival
|
|
381
|
+
let expectedCallback = 0
|
|
339
382
|
for (const [index, frame] of frames.entries()) {
|
|
383
|
+
while (superseded.delete(expectedCallback)) expectedCallback++
|
|
340
384
|
const clock = frame.consumerArrival
|
|
341
385
|
if (
|
|
342
|
-
frame.callbackIndex !==
|
|
386
|
+
frame.callbackIndex !== expectedCallback++ ||
|
|
343
387
|
media[index].sourceFrameIndex !== index ||
|
|
344
388
|
!clock ||
|
|
345
389
|
clock.domain !== 'host-mach-absolute-time' ||
|
|
@@ -370,6 +414,9 @@ function authenticateNativeCadenceCensus(root, sourceRoot, life, decoded) {
|
|
|
370
414
|
),
|
|
371
415
|
)
|
|
372
416
|
}
|
|
417
|
+
while (superseded.delete(expectedCallback)) expectedCallback++
|
|
418
|
+
if (superseded.size)
|
|
419
|
+
throw new Error('native superseded receipts fall outside the callback order')
|
|
373
420
|
}
|
|
374
421
|
|
|
375
422
|
function readDiscreteFrameReceipt(root, sourceRoot, reference, expected) {
|
|
@@ -706,9 +753,23 @@ function discreteFrameEvidenceFiles(root, evidence) {
|
|
|
706
753
|
const life = JSON.parse(
|
|
707
754
|
fs.readFileSync(proofBoundPath(root, receipt.lifecycle.path), 'utf8'),
|
|
708
755
|
)
|
|
756
|
+
// the app's ui frame clock and delivered input are replayed from their
|
|
757
|
+
// own receipts, so their bytes travel bound to the hashes the lifecycle
|
|
758
|
+
// recorded
|
|
759
|
+
for (const [label, auxiliary] of [
|
|
760
|
+
['ui frame clock', life.uiFrameClock],
|
|
761
|
+
['delivered input', life.deliveredUIKitInput],
|
|
762
|
+
]) {
|
|
763
|
+
if (!auxiliary) continue
|
|
764
|
+
const bytes = fs.readFileSync(proofBoundPath(root, auxiliary.sourceReceipt))
|
|
765
|
+
if (proofByteHash(bytes) !== auxiliary.sourceReceiptSha256)
|
|
766
|
+
throw new Error(`native ${label} receipt does not match its recorded hash`)
|
|
767
|
+
paths.add(auxiliary.sourceReceipt)
|
|
768
|
+
}
|
|
709
769
|
const native = life.nativeFramebuffer
|
|
710
770
|
if (native) {
|
|
711
771
|
paths.add(native.receipt)
|
|
772
|
+
paths.add(native.superseded)
|
|
712
773
|
paths.add(native.mediaFrames)
|
|
713
774
|
const rows = fs
|
|
714
775
|
.readFileSync(proofBoundPath(root, native.receipt), 'utf8')
|
|
@@ -104,3 +104,10 @@ export function writeDiscreteFrameReceipt(
|
|
|
104
104
|
context: DiscreteProofContext,
|
|
105
105
|
trace: DiscreteFrameTrace,
|
|
106
106
|
): DiscreteProofReference
|
|
107
|
+
|
|
108
|
+
export function boundText(
|
|
109
|
+
root: string,
|
|
110
|
+
relative: string,
|
|
111
|
+
sha256: string,
|
|
112
|
+
label: string,
|
|
113
|
+
): string
|
package/detox/expectations.ts
CHANGED
|
@@ -54,8 +54,12 @@ export function isNodeVisible(node: SootSimNodeInfo | null | undefined): boolean
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
// a native host is focused when its editor child holds focus.
|
|
57
|
-
export function focusIsWithin(
|
|
58
|
-
|
|
57
|
+
export function focusIsWithin(
|
|
58
|
+
focused: SootSimFocusedNodeInfo,
|
|
59
|
+
node: SootSimNodeInfo,
|
|
60
|
+
): boolean {
|
|
61
|
+
const nativeNodeId = node.nativeTag ?? node.nodeId
|
|
62
|
+
return focused.nodeId === nativeNodeId || focused.ancestorNodeIds.includes(nativeNodeId)
|
|
59
63
|
}
|
|
60
64
|
|
|
61
65
|
// the engine owns focus for canvas inputs and hosted native editors.
|
|
@@ -64,7 +68,7 @@ export async function isTextInputNodeFocused(
|
|
|
64
68
|
page: Page,
|
|
65
69
|
): Promise<boolean> {
|
|
66
70
|
const focused = await page.evaluate(async () => window.__sootsimTest!.getFocusedNode())
|
|
67
|
-
return focused !== null && focusIsWithin(focused, node
|
|
71
|
+
return focused !== null && focusIsWithin(focused, node)
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
async function isNodeFocused(node: SootSimNodeInfo, page: Page): Promise<boolean> {
|