rnxsim 0.1.452 → 0.1.453
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/cli/bridge-flow-runner.ts +41 -13
- package/cli/cloud-client.ts +10 -0
- package/cli/commands/control.ts +52 -7
- package/cli/commands/inspect/settling.ts +35 -3
- package/cli/drivers/playwright.ts +2 -4
- package/cli/ws-bridge.ts +72 -5
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.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 +1 -1
- 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/capture-contract.cjs +1 -1
- package/dist-lib/capture-contract.mjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.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/detox/index.cjs +1 -1
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +1 -1
- 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 +89 -8
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.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 +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/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +35 -5
- package/dist-lib/sdk.mjs +35 -5
- package/dist-lib/skills.cjs +189 -41
- package/dist-lib/vite.cjs +1 -1
- package/package.json +1 -1
- package/src/flow-timeout-scale.ts +15 -0
- package/src/playwright-host-log.ts +16 -0
- package/src/sim-client.ts +4 -0
|
@@ -6,10 +6,12 @@ import {
|
|
|
6
6
|
REQUIRED_IDENTICAL_PROOF_FRAMES,
|
|
7
7
|
framesIdentical,
|
|
8
8
|
} from '../detox/proof-frame.cjs'
|
|
9
|
+
import { flowTimeoutScale } from '../src/flow-timeout-scale'
|
|
9
10
|
import { composeFramedScreenshot } from '../src/screenshots/frame-compose'
|
|
10
11
|
import { inspectWaitReady, waitReadyReason } from './commands/inspect/core'
|
|
11
12
|
import { waitForSootsimIdle } from './commands/inspect/settling'
|
|
12
13
|
import { callShellCommandWhenReady, getShellState } from './commands/inspect/shared'
|
|
14
|
+
import { saveCurrentSimId } from './current-sim'
|
|
13
15
|
import { FlowLiveStatusReporter } from './flow-live-status'
|
|
14
16
|
import { MaestroJsContext, scriptConditionIsTruthy } from './maestro-js'
|
|
15
17
|
import { ensureCliRecordingEntitlement } from './recording-access'
|
|
@@ -21,16 +23,7 @@ import type {
|
|
|
21
23
|
} from '@rnx/globals'
|
|
22
24
|
|
|
23
25
|
const DEFAULT_TIMEOUT = 10000
|
|
24
|
-
|
|
25
|
-
// guest bundle (often 20+ MB) re-evals and re-hydrates on every launchApp hard
|
|
26
|
-
// reload, and it is served over a cloudflared tunnel. SOOTSIM_FLOW_TIMEOUT_SCALE
|
|
27
|
-
// multiplies every flow wait deadline so a slow-but-working app clears its own
|
|
28
|
-
// `.maestro` waits without editing per-app timeouts. off (1x) locally; run.sh
|
|
29
|
-
// sets it for the recording lane. clamped to 1..10.
|
|
30
|
-
const FLOW_TIMEOUT_SCALE = Math.min(
|
|
31
|
-
10,
|
|
32
|
-
Math.max(1, Number(process.env.SOOTSIM_FLOW_TIMEOUT_SCALE) || 1),
|
|
33
|
-
)
|
|
26
|
+
const FLOW_TIMEOUT_SCALE = flowTimeoutScale()
|
|
34
27
|
const scaledTimeout = (ms: number) => Math.round(ms * FLOW_TIMEOUT_SCALE)
|
|
35
28
|
const SCREEN_W = 393
|
|
36
29
|
const SCREEN_H = 852
|
|
@@ -496,11 +489,39 @@ export class SootSimBridgeFlowRunner {
|
|
|
496
489
|
}
|
|
497
490
|
|
|
498
491
|
async waitForTree(timeout: number = DEFAULT_TIMEOUT) {
|
|
499
|
-
|
|
492
|
+
let status = await inspectWaitReady(this.bridge, scaledTimeout(timeout))
|
|
500
493
|
if (status.ready) return
|
|
501
494
|
if (status.externalError) {
|
|
502
495
|
throw new Error(`app failed to load: ${status.externalError}`)
|
|
503
496
|
}
|
|
497
|
+
if (status.bridgeError) {
|
|
498
|
+
// the guest re-registers under a new sim id whenever its page reloads,
|
|
499
|
+
// and every command here pins this.opts.simId, so a reload strands the
|
|
500
|
+
// whole flow on a disposed render host. re-pin to the same browser
|
|
501
|
+
// host's successor and read the tree again before calling it dead.
|
|
502
|
+
// the successor does not appear the instant the old sim is disposed —
|
|
503
|
+
// the page has to finish reloading and re-register — so give it the same
|
|
504
|
+
// budget the rest of the flow waits on rather than looking exactly once.
|
|
505
|
+
const pinned = this.opts.simId
|
|
506
|
+
let successor: string | null = null
|
|
507
|
+
if (pinned) {
|
|
508
|
+
const deadline = Date.now() + scaledTimeout(15000)
|
|
509
|
+
do {
|
|
510
|
+
successor = await this.bridge.resolveReloadedSim(pinned)
|
|
511
|
+
if (successor) break
|
|
512
|
+
await sleep(500)
|
|
513
|
+
} while (Date.now() < deadline)
|
|
514
|
+
}
|
|
515
|
+
if (successor) {
|
|
516
|
+
console.error(
|
|
517
|
+
` note: sim ${pinned} reloaded and reconnected as ${successor}; following it`,
|
|
518
|
+
)
|
|
519
|
+
this.opts.simId = successor
|
|
520
|
+
saveCurrentSimId(successor)
|
|
521
|
+
status = await inspectWaitReady(this.bridge, scaledTimeout(timeout))
|
|
522
|
+
if (status.ready) return
|
|
523
|
+
}
|
|
524
|
+
}
|
|
504
525
|
if (status.bridgeError) {
|
|
505
526
|
throw new Error(
|
|
506
527
|
`target sim is not responding to the bridge (${status.bridgeError}). ` +
|
|
@@ -1989,7 +2010,9 @@ export class SootSimBridgeFlowRunner {
|
|
|
1989
2010
|
strict: true,
|
|
1990
2011
|
})
|
|
1991
2012
|
if (!result.settled) {
|
|
1992
|
-
throw new Error(
|
|
2013
|
+
throw new Error(
|
|
2014
|
+
`visual state did not settle within ${result.elapsed}ms (${result.blockedBy})`,
|
|
2015
|
+
)
|
|
1993
2016
|
}
|
|
1994
2017
|
}
|
|
1995
2018
|
|
|
@@ -2290,7 +2313,12 @@ export class SootSimBridgeFlowRunner {
|
|
|
2290
2313
|
step.runFlow ||
|
|
2291
2314
|
// runScript http calls are synchronous with a 5-minute upstream
|
|
2292
2315
|
// timeout — the 10s watchdog would kill legitimate setup scripts.
|
|
2293
|
-
step.runScript
|
|
2316
|
+
step.runScript ||
|
|
2317
|
+
// takeScreenshot self-governs: it retries a bounded visual settle that
|
|
2318
|
+
// already fails with its own reason. sharing the watchdog's 10s budget
|
|
2319
|
+
// meant the watchdog always won that race, reporting "bridge probably
|
|
2320
|
+
// hung" for a screen that was merely still animating.
|
|
2321
|
+
step.takeScreenshot
|
|
2294
2322
|
)
|
|
2295
2323
|
const STEP_WATCHDOG_MS = 10_000
|
|
2296
2324
|
const body = this.runStepInner(step)
|
package/cli/cloud-client.ts
CHANGED
|
@@ -1001,6 +1001,11 @@ class CloudBridge implements WsBridge {
|
|
|
1001
1001
|
return Promise.reject(new Error('remote simulators have no browser window to focus'))
|
|
1002
1002
|
}
|
|
1003
1003
|
|
|
1004
|
+
resolveReloadedSim(): Promise<string | null> {
|
|
1005
|
+
// cloud sims carry no local browser host, so there is no lineage to follow
|
|
1006
|
+
return Promise.resolve(null)
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1004
1009
|
closeSim(): Promise<never> {
|
|
1005
1010
|
return Promise.reject(
|
|
1006
1011
|
new Error('remote simulator close uses the simulator DELETE route'),
|
|
@@ -1091,6 +1096,11 @@ class CloudBoxBridge implements WsBridge {
|
|
|
1091
1096
|
return Promise.reject(new Error('a box simulator has no browser window to focus'))
|
|
1092
1097
|
}
|
|
1093
1098
|
|
|
1099
|
+
resolveReloadedSim(): Promise<string | null> {
|
|
1100
|
+
// cloud sims carry no local browser host, so there is no lineage to follow
|
|
1101
|
+
return Promise.resolve(null)
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1094
1104
|
closeSim(): Promise<never> {
|
|
1095
1105
|
return Promise.reject(new Error('close a box simulator with rnx box stop'))
|
|
1096
1106
|
}
|
package/cli/commands/control.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
resolveConnectionInput,
|
|
23
23
|
type ResolvedDevBundle,
|
|
24
24
|
} from '../../src/dev-bundle-resolution'
|
|
25
|
+
import { flowTimeoutScale } from '../../src/flow-timeout-scale'
|
|
25
26
|
import {
|
|
26
27
|
isDaemonLockfileFresh,
|
|
27
28
|
isDevBridgeLockfileFresh,
|
|
@@ -844,10 +845,21 @@ async function waitForSimReady(
|
|
|
844
845
|
minNodeCount?: number
|
|
845
846
|
} = {},
|
|
846
847
|
) {
|
|
847
|
-
|
|
848
|
+
// the ceiling here is attempts * intervalMs. a hosted launchApp re-evals the
|
|
849
|
+
// whole guest bundle over a tunnel, which routinely outlasts the local
|
|
850
|
+
// default, so scale the poll ceiling by the same factor the flow runner
|
|
851
|
+
// scales its own waits. this still returns the moment the sim reports enough
|
|
852
|
+
// nodes; only the deadline moves, and locally the scale is 1.
|
|
848
853
|
const intervalMs = opts.intervalMs ?? 500
|
|
854
|
+
const attempts = opts.attempts ?? Math.round(30 * flowTimeoutScale())
|
|
849
855
|
const minNodeCount = opts.minNodeCount ?? 10
|
|
850
856
|
|
|
857
|
+
// a poll that only ever reports "timed out" cannot say whether the guest was
|
|
858
|
+
// still booting, never installed the test hook, or was unreachable, so keep
|
|
859
|
+
// the last observation and hand it to the caller for the failure message.
|
|
860
|
+
let lastCount: unknown
|
|
861
|
+
let lastError = ''
|
|
862
|
+
|
|
851
863
|
for (let i = 0; i < attempts; i++) {
|
|
852
864
|
const bridge = createBridge(wsPort, {
|
|
853
865
|
commandTimeoutMs,
|
|
@@ -857,19 +869,21 @@ async function waitForSimReady(
|
|
|
857
869
|
try {
|
|
858
870
|
const count = await bridge.send({
|
|
859
871
|
type: 'evaluate',
|
|
860
|
-
code: '(async () => (await window.__sootsimTest?.getNodeCount())
|
|
872
|
+
code: '(async () => (await window.__sootsimTest?.getNodeCount()) ?? null)()',
|
|
861
873
|
})
|
|
874
|
+
lastCount = count
|
|
875
|
+
lastError = ''
|
|
862
876
|
if (typeof count === 'number' && count > minNodeCount) {
|
|
863
877
|
return { bridge, count }
|
|
864
878
|
}
|
|
865
|
-
} catch {
|
|
866
|
-
|
|
879
|
+
} catch (err) {
|
|
880
|
+
lastError = err instanceof Error ? err.message : String(err)
|
|
867
881
|
}
|
|
868
882
|
bridge.close()
|
|
869
883
|
await sleep(intervalMs)
|
|
870
884
|
}
|
|
871
885
|
|
|
872
|
-
return null
|
|
886
|
+
return { bridge: null, attempts, intervalMs, minNodeCount, lastCount, lastError }
|
|
873
887
|
}
|
|
874
888
|
|
|
875
889
|
export async function waitForSimMatch(
|
|
@@ -1511,6 +1525,7 @@ export async function runOpenCommand(
|
|
|
1511
1525
|
const bridge = createBridgeFromParsed(parsed)
|
|
1512
1526
|
const simHint = parsed.simId ? ` --sim ${parsed.simId}` : ''
|
|
1513
1527
|
let fallBackToNewSim = false
|
|
1528
|
+
let preNavSimIds = new Set<string>()
|
|
1514
1529
|
|
|
1515
1530
|
try {
|
|
1516
1531
|
let targetSim: BridgeSimInfo | null = null
|
|
@@ -1570,6 +1585,11 @@ export async function runOpenCommand(
|
|
|
1570
1585
|
}
|
|
1571
1586
|
}
|
|
1572
1587
|
if (!fallBackToNewSim && targetSim) {
|
|
1588
|
+
// a navigation that crosses origins tears down the page context, so
|
|
1589
|
+
// the guest reconnects as a NEW sim and the id we are holding stops
|
|
1590
|
+
// existing. record what was connected first, so the wait below can
|
|
1591
|
+
// follow the reload instead of polling an id the browser retired.
|
|
1592
|
+
preNavSimIds = new Set(sims.map((sim) => sim.id))
|
|
1573
1593
|
const resolvedBaseUrl =
|
|
1574
1594
|
hasExplicitBaseUrl || looksLikeSootsimUrl(target)
|
|
1575
1595
|
? baseUrl
|
|
@@ -1603,13 +1623,38 @@ export async function runOpenCommand(
|
|
|
1603
1623
|
|
|
1604
1624
|
if (!fallBackToNewSim && targetSim) {
|
|
1605
1625
|
await sleep(1500)
|
|
1626
|
+
const navSim = targetSim
|
|
1627
|
+
const reloadedSim = await waitForSimMatch(
|
|
1628
|
+
parsed.wsPort,
|
|
1629
|
+
parsed.commandTimeoutMs,
|
|
1630
|
+
(sim) =>
|
|
1631
|
+
sim.readyState === 'open' &&
|
|
1632
|
+
simUsesDriver(sim, driverId) &&
|
|
1633
|
+
(sim.id === navSim.id || (!preNavSimIds.has(sim.id) && sim.isPrimary)),
|
|
1634
|
+
{ attempts: Math.round(30 * flowTimeoutScale()) },
|
|
1635
|
+
)
|
|
1636
|
+
if (reloadedSim && reloadedSim.id !== navSim.id) {
|
|
1637
|
+
targetSim = reloadedSim
|
|
1638
|
+
}
|
|
1606
1639
|
const ready = await waitForSimReady(
|
|
1607
1640
|
parsed.wsPort,
|
|
1608
1641
|
parsed.commandTimeoutMs,
|
|
1609
1642
|
targetSim.id,
|
|
1610
1643
|
)
|
|
1611
|
-
if (!ready) {
|
|
1612
|
-
|
|
1644
|
+
if (!ready.bridge) {
|
|
1645
|
+
const observed = ready.lastError
|
|
1646
|
+
? `last bridge error: ${ready.lastError}`
|
|
1647
|
+
: ready.lastCount === null
|
|
1648
|
+
? 'guest never installed window.__sootsimTest'
|
|
1649
|
+
: `last node count: ${String(ready.lastCount)} (needs > ${ready.minNodeCount})`
|
|
1650
|
+
console.error(
|
|
1651
|
+
` timed out waiting for current sim to load target after ${ready.attempts} polls over ${Math.round((ready.attempts * ready.intervalMs) / 1000)}s; ${observed}`,
|
|
1652
|
+
)
|
|
1653
|
+
await printBridgeFailureDiagnostics(bridge, {
|
|
1654
|
+
errorsCommand: `rnx get errors 5${simHint}`,
|
|
1655
|
+
warningsCommand: `rnx get warnings 5${simHint}`,
|
|
1656
|
+
requestsCommand: `rnx get requests 5${simHint}`,
|
|
1657
|
+
})
|
|
1613
1658
|
rnxExit(1)
|
|
1614
1659
|
}
|
|
1615
1660
|
ready.bridge.close()
|
|
@@ -12,6 +12,10 @@ export type WaitForSootsimIdleOptions = {
|
|
|
12
12
|
export type WaitForSootsimIdleResult = {
|
|
13
13
|
elapsed: number
|
|
14
14
|
settled: boolean
|
|
15
|
+
// what the last poll still saw moving. a settle that only reports "did not
|
|
16
|
+
// settle" cannot tell a slow screen from one that animates forever, and the
|
|
17
|
+
// two need opposite fixes.
|
|
18
|
+
blockedBy: string
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
export async function waitForSootsimIdle({
|
|
@@ -117,8 +121,19 @@ export async function waitForSootsimIdle({
|
|
|
117
121
|
let lastRequestTotal = -1
|
|
118
122
|
let stable = 0
|
|
119
123
|
let visuallyStillSince = 0
|
|
124
|
+
// counts across the whole budget: a flag that is true on every poll is a
|
|
125
|
+
// different bug from one that is true intermittently, and the two need
|
|
126
|
+
// opposite fixes (engine vs budget).
|
|
127
|
+
let polls = 0
|
|
128
|
+
let dirtyPolls = 0
|
|
129
|
+
let animatingPolls = 0
|
|
130
|
+
let layoutChangedPolls = 0
|
|
120
131
|
while (Date.now() < deadline) {
|
|
121
132
|
const snapshot = await readSnapshot()
|
|
133
|
+
polls++
|
|
134
|
+
if (snapshot.layoutDirty) dirtyPolls++
|
|
135
|
+
if (snapshot.animating) animatingPolls++
|
|
136
|
+
if (polls > 1 && snapshot.layout !== lastLayout) layoutChangedPolls++
|
|
122
137
|
const strictOk =
|
|
123
138
|
!strict ||
|
|
124
139
|
(snapshot.renderStatsAvailable && !snapshot.animating && !snapshot.layoutDirty)
|
|
@@ -139,7 +154,7 @@ export async function waitForSootsimIdle({
|
|
|
139
154
|
stable >= requiredStablePolls &&
|
|
140
155
|
(networkQuiet || Date.now() - visuallyStillSince >= networkQuietGraceMs)
|
|
141
156
|
) {
|
|
142
|
-
return { settled: true, elapsed: Date.now() - start }
|
|
157
|
+
return { settled: true, elapsed: Date.now() - start, blockedBy: '' }
|
|
143
158
|
}
|
|
144
159
|
} else {
|
|
145
160
|
stable = 0
|
|
@@ -149,7 +164,22 @@ export async function waitForSootsimIdle({
|
|
|
149
164
|
lastRequestTotal = snapshot.requestTotal
|
|
150
165
|
await sleep(pollMs)
|
|
151
166
|
}
|
|
152
|
-
|
|
167
|
+
const last = await readSnapshot()
|
|
168
|
+
const blockers = []
|
|
169
|
+
if (strict && !last.renderStatsAvailable) blockers.push('render stats unavailable')
|
|
170
|
+
if (last.animating) blockers.push('animations still running')
|
|
171
|
+
if (last.layoutDirty) blockers.push('layout still dirty')
|
|
172
|
+
if (last.pendingFetches > 0) blockers.push(last.pendingFetches + ' image fetches pending')
|
|
173
|
+
if (last.layout !== lastLayout) blockers.push('layout still changing')
|
|
174
|
+
if (last.requestInFlight > 0) blockers.push(last.requestInFlight + ' requests in flight')
|
|
175
|
+
return {
|
|
176
|
+
settled: false,
|
|
177
|
+
elapsed: Date.now() - start,
|
|
178
|
+
blockedBy:
|
|
179
|
+
(blockers.length ? blockers.join(', ') : 'unknown') +
|
|
180
|
+
' over ' + polls + ' polls (layout changed ' + layoutChangedPolls +
|
|
181
|
+
'x, layoutDirty ' + dirtyPolls + 'x, animating ' + animatingPolls + 'x)',
|
|
182
|
+
}
|
|
153
183
|
})()`,
|
|
154
184
|
},
|
|
155
185
|
{
|
|
@@ -157,9 +187,11 @@ export async function waitForSootsimIdle({
|
|
|
157
187
|
},
|
|
158
188
|
)
|
|
159
189
|
|
|
160
|
-
const { elapsed, settled } = (result ??
|
|
190
|
+
const { elapsed, settled, blockedBy } = (result ??
|
|
191
|
+
{}) as Partial<WaitForSootsimIdleResult>
|
|
161
192
|
return {
|
|
162
193
|
elapsed: typeof elapsed === 'number' ? elapsed : maxMs,
|
|
163
194
|
settled: settled === true,
|
|
195
|
+
blockedBy: typeof blockedBy === 'string' ? blockedBy : 'unknown',
|
|
164
196
|
}
|
|
165
197
|
}
|
|
@@ -16,6 +16,7 @@ import { spawn } from 'child_process'
|
|
|
16
16
|
import { closeSync, mkdtempSync, openSync, readFileSync } from 'fs'
|
|
17
17
|
import { tmpdir } from 'os'
|
|
18
18
|
import { join } from 'path'
|
|
19
|
+
import { playwrightHostLogName } from '../../src/playwright-host-log'
|
|
19
20
|
import { ensureProfile, playwrightProfileUserDataDir } from '../../src/profiles'
|
|
20
21
|
import { RNX_INTERNAL_CHILDREN, RNX_INTERNAL_COMMAND } from '../internal-child'
|
|
21
22
|
import { getProcessComm } from '../parent-pid'
|
|
@@ -102,10 +103,7 @@ async function launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult> {
|
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
const errLog = join(
|
|
106
|
-
tmpdir(),
|
|
107
|
-
`rnx-playwright-host-${Date.now().toString(36)}-${process.pid}.log`,
|
|
108
|
-
)
|
|
106
|
+
const errLog = join(tmpdir(), playwrightHostLogName(Date.now(), process.pid))
|
|
109
107
|
const connectAckFile = `${errLog}.connected`
|
|
110
108
|
const errFd = openSync(errLog, 'a')
|
|
111
109
|
try {
|
package/cli/ws-bridge.ts
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
getCliIdentity,
|
|
29
29
|
readCurrentSim,
|
|
30
30
|
readCurrentSimId,
|
|
31
|
+
saveCurrentSimId,
|
|
31
32
|
} from './current-sim'
|
|
32
33
|
|
|
33
34
|
// --- bridge world resolution ---------------------------------------------
|
|
@@ -323,6 +324,10 @@ export interface WsBridge {
|
|
|
323
324
|
opts?: { timeoutMs?: number },
|
|
324
325
|
): Promise<any>
|
|
325
326
|
listSims(): Promise<BridgeSimInfo[]>
|
|
327
|
+
// a guest reload retires the sim id and re-registers the same page under a
|
|
328
|
+
// new one. a caller that pins an id resolves the successor here; null means
|
|
329
|
+
// nothing proves what replaced it.
|
|
330
|
+
resolveReloadedSim(retiredSimId: string): Promise<string | null>
|
|
326
331
|
focusSim(simId?: string): Promise<any>
|
|
327
332
|
closeSim(simId?: string): Promise<any>
|
|
328
333
|
claim(simId?: string, opts?: { force?: boolean }): Promise<BridgeClaimResult>
|
|
@@ -611,6 +616,24 @@ export function createBridge(wsPort: number, opts: WsBridgeOptions = {}): WsBrid
|
|
|
611
616
|
|
|
612
617
|
let warnedAboutContention = false
|
|
613
618
|
let noticedTarget = false
|
|
619
|
+
// the browser host behind the current pin, learned while the pinned sim is
|
|
620
|
+
// still connected. a reload changes the sim id but not the host.
|
|
621
|
+
let pinnedHostPid: number | null = null
|
|
622
|
+
let lineageLookupInFlight = false
|
|
623
|
+
|
|
624
|
+
// a reload retires the sim id but keeps the browser host, so a successor
|
|
625
|
+
// sharing that host pid IS the same page lineage rather than a guess. only
|
|
626
|
+
// an unambiguous match counts; callers fail closed on null.
|
|
627
|
+
function pickReloadedSuccessor(sims: unknown, retiredSimId: string): string | null {
|
|
628
|
+
if (pinnedHostPid === null) return null
|
|
629
|
+
const successors = (Array.isArray(sims) ? (sims as BridgeSimInfo[]) : []).filter(
|
|
630
|
+
(sim) =>
|
|
631
|
+
sim.id !== retiredSimId &&
|
|
632
|
+
sim.readyState === 'open' &&
|
|
633
|
+
sim.meta?.sootsimHostPid === pinnedHostPid,
|
|
634
|
+
)
|
|
635
|
+
return successors.length === 1 ? successors[0].id : null
|
|
636
|
+
}
|
|
614
637
|
|
|
615
638
|
// print a one-line notice on first sim-scoped command so agents/users see
|
|
616
639
|
// which sim the command actually hit. suppressed for bridge:*
|
|
@@ -682,7 +705,10 @@ export function createBridge(wsPort: number, opts: WsBridgeOptions = {}): WsBrid
|
|
|
682
705
|
// NOTE: `opts` (the createBridge closure arg) carries simId; do not
|
|
683
706
|
// shadow it with the per-call arg.
|
|
684
707
|
const effectiveTimeoutMs = callOpts?.timeoutMs ?? commandTimeoutMs
|
|
685
|
-
const sendOnce = async (
|
|
708
|
+
const sendOnce = async (
|
|
709
|
+
defaultSimId?: string,
|
|
710
|
+
cmdOverride?: Record<string, any>,
|
|
711
|
+
) => {
|
|
686
712
|
await ready
|
|
687
713
|
const id = nextId++
|
|
688
714
|
return new Promise((resolve, reject) => {
|
|
@@ -706,7 +732,7 @@ export function createBridge(wsPort: number, opts: WsBridgeOptions = {}): WsBrid
|
|
|
706
732
|
},
|
|
707
733
|
})
|
|
708
734
|
|
|
709
|
-
const payload = { ...cmd, id } as Record<string, any>
|
|
735
|
+
const payload = { ...(cmdOverride ?? cmd), id } as Record<string, any>
|
|
710
736
|
if (payload.simId === undefined && defaultSimId) {
|
|
711
737
|
payload.simId = defaultSimId
|
|
712
738
|
}
|
|
@@ -725,7 +751,26 @@ export function createBridge(wsPort: number, opts: WsBridgeOptions = {}): WsBrid
|
|
|
725
751
|
const sentSimId =
|
|
726
752
|
opts.simIdSource === 'flag' ? opts.simId : (readCurrentSimId() ?? opts.simId)
|
|
727
753
|
try {
|
|
728
|
-
|
|
754
|
+
const result = await sendOnce(sentSimId)
|
|
755
|
+
// the guest re-registers under a NEW sim id whenever its page reloads,
|
|
756
|
+
// so remember which browser host is behind the pin while it still
|
|
757
|
+
// exists. that lineage is what lets the fail-closed path below follow
|
|
758
|
+
// the reload without ever guessing at an unrelated sim.
|
|
759
|
+
if (sentSimId && pinnedHostPid === null && !lineageLookupInFlight) {
|
|
760
|
+
lineageLookupInFlight = true
|
|
761
|
+
void sendOnce(undefined, { type: 'bridge:list-sims' })
|
|
762
|
+
.then((sims) => {
|
|
763
|
+
if (!Array.isArray(sims)) return
|
|
764
|
+
const pinned = (sims as BridgeSimInfo[]).find((sim) => sim.id === sentSimId)
|
|
765
|
+
const hostPid = pinned?.meta?.sootsimHostPid
|
|
766
|
+
if (typeof hostPid === 'number') pinnedHostPid = hostPid
|
|
767
|
+
})
|
|
768
|
+
.catch(() => {})
|
|
769
|
+
.finally(() => {
|
|
770
|
+
lineageLookupInFlight = false
|
|
771
|
+
})
|
|
772
|
+
}
|
|
773
|
+
return result
|
|
729
774
|
} catch (error) {
|
|
730
775
|
const message = error instanceof Error ? error.message : String(error)
|
|
731
776
|
const missingSimMessage = `no sim connected with id ${sentSimId}`
|
|
@@ -735,9 +780,24 @@ export function createBridge(wsPort: number, opts: WsBridgeOptions = {}): WsBrid
|
|
|
735
780
|
cmd.simId === undefined &&
|
|
736
781
|
(message === missingSimMessage || message.startsWith(`${missingSimMessage};`))
|
|
737
782
|
) {
|
|
783
|
+
// a reload retires the sim id but keeps the browser host, so the
|
|
784
|
+
// successor with the same host pid IS the same page lineage — not a
|
|
785
|
+
// guess. adopt only that, and only when it is unambiguous.
|
|
786
|
+
const successor = pickReloadedSuccessor(
|
|
787
|
+
await sendOnce(undefined, { type: 'bridge:list-sims' }),
|
|
788
|
+
sentSimId,
|
|
789
|
+
)
|
|
790
|
+
if (successor) {
|
|
791
|
+
saveCurrentSimId(successor)
|
|
792
|
+
process.stderr.write(
|
|
793
|
+
` note: sim ${sentSimId} reloaded and reconnected as ${successor}; following it\n`,
|
|
794
|
+
)
|
|
795
|
+
return sendOnce(successor)
|
|
796
|
+
}
|
|
738
797
|
clearCurrentSimId()
|
|
739
|
-
// the saved sim is gone
|
|
740
|
-
// sim is primary; that is how
|
|
798
|
+
// the saved sim is gone and nothing proves what replaced it. fail
|
|
799
|
+
// closed instead of driving whichever sim is primary; that is how
|
|
800
|
+
// agents end up recording the wrong app.
|
|
741
801
|
throw new Error(
|
|
742
802
|
`saved sim ${sentSimId} is gone${message.slice(missingSimMessage.length)}; ` +
|
|
743
803
|
'run `rnx list` and ' +
|
|
@@ -751,6 +811,13 @@ export function createBridge(wsPort: number, opts: WsBridgeOptions = {}): WsBrid
|
|
|
751
811
|
const sims = await this.send({ type: 'bridge:list-sims' })
|
|
752
812
|
return Array.isArray(sims) ? (sims as BridgeSimInfo[]) : []
|
|
753
813
|
},
|
|
814
|
+
// a caller holding its own pinned sim id (a flow runner sets simId on every
|
|
815
|
+
// command, so it never reaches the pin-following path in send) asks here
|
|
816
|
+
// after a reload retires that id. null means nothing proves what replaced
|
|
817
|
+
// it, and the caller must fail rather than guess.
|
|
818
|
+
async resolveReloadedSim(retiredSimId: string) {
|
|
819
|
+
return pickReloadedSuccessor(await this.listSims(), retiredSimId)
|
|
820
|
+
},
|
|
754
821
|
async focusSim(simId?: string) {
|
|
755
822
|
return this.send({ type: 'focus', simId })
|
|
756
823
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.453 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.453 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.453 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.453 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.453 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.453 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.453 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/beta.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.453 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/beta.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.453 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.453 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.453 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.453 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.453 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.453 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/cloud.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.453 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/cloud.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.453 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
4
4
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
package/dist-lib/config.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.453 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/detox/index.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.453 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.453 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __defProp = Object.defineProperty;
|
package/dist-lib/home-paths.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.453 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
|
|
2
2
|
let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
|
|
3
3
|
"use strict";
|
|
4
4
|
var __create = Object.create;
|