rnxsim 0.1.354 → 0.1.356

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/cli/bridge-flow-runner.ts +117 -75
  2. package/cli/commands/inspect/settling.ts +9 -2
  3. package/cli/commands/maestro.ts +9 -4
  4. package/cli/outbound-endpoints.ts +1 -1
  5. package/detox/index.ts +144 -74
  6. package/detox/proof-frame.cjs +107 -0
  7. package/dist-cli/bin.js +5 -5
  8. package/dist-cli/chunks/{agent-FB7RPXO6.js → agent-RHSVLLYV.js} +2 -2
  9. package/dist-cli/chunks/{agent-wrapper-LFU3XEIG.js → agent-wrapper-76J2FUFC.js} +2 -2
  10. package/dist-cli/chunks/{app-fonts-FK6SETRG.js → app-fonts-NXSVJTJO.js} +2 -2
  11. package/dist-cli/chunks/{assert-3KQYZDDK.js → assert-2EYNC3CJ.js} +2 -2
  12. package/dist-cli/chunks/{auth-RFE3J27C.js → auth-BPTC5SFR.js} +2 -2
  13. package/dist-cli/chunks/{beta-XVY4WOYH.js → beta-SSB7KYRA.js} +2 -2
  14. package/dist-cli/chunks/{camera-JROBX5GM.js → camera-VVTXBQXD.js} +2 -2
  15. package/dist-cli/chunks/{chunk-YIUIFZAE.js → chunk-2UIMQVMQ.js} +2 -2
  16. package/dist-cli/chunks/{chunk-B7OMSJXC.js → chunk-3OCUYKVX.js} +2 -2
  17. package/dist-cli/chunks/{chunk-ZNABCRWX.js → chunk-3QQFGK3A.js} +2 -2
  18. package/dist-cli/chunks/{chunk-3QLERZYJ.js → chunk-3SYKCLCM.js} +2 -2
  19. package/dist-cli/chunks/{chunk-BNXXNT3I.js → chunk-4HQXS5PW.js} +2 -2
  20. package/dist-cli/chunks/{chunk-P4GCLNFM.js → chunk-4V6JMAXO.js} +2 -2
  21. package/dist-cli/chunks/{chunk-SLD2EMLZ.js → chunk-5AF3BFLN.js} +2 -2
  22. package/dist-cli/chunks/{chunk-UI2IFVHB.js → chunk-5AKKSWL5.js} +1 -1
  23. package/dist-cli/chunks/{chunk-THFM2CFT.js → chunk-5DAVGCVR.js} +1 -1
  24. package/dist-cli/chunks/{chunk-CKKDXTKV.js → chunk-6CGOQKVQ.js} +1 -1
  25. package/dist-cli/chunks/{chunk-766UIGCS.js → chunk-6TGSJGCM.js} +2 -2
  26. package/dist-cli/chunks/{chunk-ATQPDEED.js → chunk-6TWYMJCB.js} +2 -2
  27. package/dist-cli/chunks/{chunk-LRVL5MAX.js → chunk-7E22W7XX.js} +1 -1
  28. package/dist-cli/chunks/{chunk-CUK6Y2RR.js → chunk-7EAA32OO.js} +2 -2
  29. package/dist-cli/chunks/{chunk-UX6D4X6B.js → chunk-A2F5B2AB.js} +2 -2
  30. package/dist-cli/chunks/{chunk-HUU7ZUQK.js → chunk-AA7PHU2M.js} +2 -2
  31. package/dist-cli/chunks/{chunk-3RW6W3ZO.js → chunk-BMMSSIRU.js} +1 -1
  32. package/dist-cli/chunks/{chunk-JT4IMNCO.js → chunk-BNZHWG6Z.js} +2 -2
  33. package/dist-cli/chunks/{chunk-UAYXEP74.js → chunk-BXT37FPN.js} +1 -1
  34. package/dist-cli/chunks/{chunk-QQNNY43R.js → chunk-BZTXJUGV.js} +1 -1
  35. package/dist-cli/chunks/{chunk-ENPWD62S.js → chunk-CK4C6GPT.js} +2 -2
  36. package/dist-cli/chunks/{chunk-ZJUFI3R7.js → chunk-DRHNOXRS.js} +1 -1
  37. package/dist-cli/chunks/{chunk-MIK6LMG4.js → chunk-E5ALNGXA.js} +1 -1
  38. package/dist-cli/chunks/{chunk-FPH4TYZO.js → chunk-E7DVCUK4.js} +3 -3
  39. package/dist-cli/chunks/{chunk-7WANPS3W.js → chunk-EVQNKPUK.js} +2 -2
  40. package/dist-cli/chunks/{chunk-QRYTCPNO.js → chunk-FAUFZBRY.js} +2 -2
  41. package/dist-cli/chunks/chunk-FVKAF4VB.js +535 -0
  42. package/dist-cli/chunks/{chunk-JWXAXCC6.js → chunk-FWBQKEAA.js} +2 -2
  43. package/dist-cli/chunks/{chunk-IGV4KCCM.js → chunk-G2SHGCDI.js} +2 -2
  44. package/dist-cli/chunks/{chunk-PHIYWV44.js → chunk-GM6YYN6P.js} +2 -2
  45. package/dist-cli/chunks/{chunk-UVIWE3LD.js → chunk-I3G6YW6L.js} +1 -1
  46. package/dist-cli/chunks/{chunk-HIYCRDUP.js → chunk-IUQL37TT.js} +2 -2
  47. package/dist-cli/chunks/{chunk-LCPZNPIB.js → chunk-JU3ISZ7X.js} +1 -1
  48. package/dist-cli/chunks/{chunk-4OS3SXPZ.js → chunk-NPH2KUL6.js} +2 -2
  49. package/dist-cli/chunks/{chunk-77CBLMD4.js → chunk-NSEPQLZF.js} +1 -1
  50. package/dist-cli/chunks/{chunk-OKFSCCFC.js → chunk-OUMGSDHQ.js} +1 -1
  51. package/dist-cli/chunks/{chunk-IUL2WUWW.js → chunk-P5F5PM6C.js} +2 -2
  52. package/dist-cli/chunks/{chunk-GNKXRXND.js → chunk-RBQUFMLX.js} +2 -2
  53. package/dist-cli/chunks/{chunk-Y4GUWO3X.js → chunk-RFSFQUUJ.js} +1 -1
  54. package/dist-cli/chunks/{chunk-TJRKLRNP.js → chunk-RQZCBNCD.js} +1 -1
  55. package/dist-cli/chunks/{chunk-LC77IVGY.js → chunk-SZD4NAQR.js} +2 -2
  56. package/dist-cli/chunks/{chunk-37S36DFW.js → chunk-T33T5MW2.js} +1 -1
  57. package/dist-cli/chunks/{chunk-4AQCDL24.js → chunk-TLA6GLYI.js} +2 -2
  58. package/dist-cli/chunks/{chunk-JT435OOM.js → chunk-TV5LN34R.js} +2 -2
  59. package/dist-cli/chunks/{chunk-EWPX6LEN.js → chunk-UC6Y77PK.js} +2 -2
  60. package/dist-cli/chunks/{chunk-IICFLREN.js → chunk-UUD5NLLB.js} +1 -1
  61. package/dist-cli/chunks/{chunk-GYGDIT5V.js → chunk-VIEN2LIY.js} +2 -2
  62. package/dist-cli/chunks/{chunk-X2DJK3TB.js → chunk-VSADIZVW.js} +2 -2
  63. package/dist-cli/chunks/{chunk-JXNR2KAB.js → chunk-VZKPDDYL.js} +2 -2
  64. package/dist-cli/chunks/{chunk-YXBNGSG6.js → chunk-X2YH3SPS.js} +1 -1
  65. package/dist-cli/chunks/{chunk-LETVUC6M.js → chunk-XJ2LMLUP.js} +1 -1
  66. package/dist-cli/chunks/{chunk-24DA3Y2Z.js → chunk-XMNBS5CX.js} +1 -1
  67. package/dist-cli/chunks/{chunk-JJOHJNUQ.js → chunk-XT6FA7AH.js} +2 -2
  68. package/dist-cli/chunks/chunk-Z6CATT5W.js +4 -0
  69. package/dist-cli/chunks/{chunk-N3CE6AZP.js → chunk-ZK67ALEG.js} +2 -2
  70. package/dist-cli/chunks/{chunk-FEHHM2LV.js → chunk-ZME75NBJ.js} +13 -6
  71. package/dist-cli/chunks/{cleanup-F74ISXJC.js → cleanup-E7TDW64X.js} +2 -2
  72. package/dist-cli/chunks/cli-version-HUIGZMTX.js +4 -0
  73. package/dist-cli/chunks/{compat-C3RGYGXL.js → compat-ST5BY7FP.js} +2 -2
  74. package/dist-cli/chunks/{config-CDY2RB7Q.js → config-Z5NEHXI7.js} +2 -2
  75. package/dist-cli/chunks/{control-J35HAFZA.js → control-JMDFMAHT.js} +2 -2
  76. package/dist-cli/chunks/daemon-VIL2M7RE.js +4 -0
  77. package/dist-cli/chunks/{debug-3P2DUTUQ.js → debug-DO2G2BA7.js} +3 -3
  78. package/dist-cli/chunks/{desktop-XIXW34VW.js → desktop-M3UETMNI.js} +3 -3
  79. package/dist-cli/chunks/{detox-KAKTJ4P4.js → detox-7JI56JTD.js} +2 -2
  80. package/dist-cli/chunks/{device-4MAWLZMR.js → device-YTRCB26J.js} +2 -2
  81. package/dist-cli/chunks/{diagnose-A3R7HUYA.js → diagnose-BJT3RXDI.js} +2 -2
  82. package/dist-cli/chunks/{disk-cleanup-5R7JYCO6.js → disk-cleanup-HWA4ATEL.js} +2 -2
  83. package/dist-cli/chunks/drivers-HIKMT3EH.js +4 -0
  84. package/dist-cli/chunks/{film-GY5U752Q.js → film-GLGYFHGZ.js} +3 -3
  85. package/dist-cli/chunks/flow-AJT2OYLT.js +4 -0
  86. package/dist-cli/chunks/help-DSDQRHN3.js +4 -0
  87. package/dist-cli/chunks/{hidden-runtime-alias-HSGN3GN4.js → hidden-runtime-alias-A4TMRHFA.js} +2 -2
  88. package/dist-cli/chunks/{home-paths-BIUN4CBO.js → home-paths-A2ANFZHA.js} +2 -2
  89. package/dist-cli/chunks/inspect-NXE6LUT6.js +4 -0
  90. package/dist-cli/chunks/install-desktop-DMTXQF3R.js +4 -0
  91. package/dist-cli/chunks/{login-WKNMS4GG.js → login-OWD44PHN.js} +2 -2
  92. package/dist-cli/chunks/{logout-6UFUOESK.js → logout-WV5QPIUW.js} +2 -2
  93. package/dist-cli/chunks/{maestro-SJGV7B3W.js → maestro-AQ342FXT.js} +18 -13
  94. package/dist-cli/chunks/{maestro-generate-PMF2LLKX.js → maestro-generate-AUTBK5QS.js} +3 -3
  95. package/dist-cli/chunks/{mode-MN7LZCLB.js → mode-XICLRQZH.js} +2 -2
  96. package/dist-cli/chunks/{optional-demo-registry-HFA27YSP.js → optional-demo-registry-FOXLAYLM.js} +2 -2
  97. package/dist-cli/chunks/{perf-XGANSJT3.js → perf-PUT7IITA.js} +2 -2
  98. package/dist-cli/chunks/{permissions-25UWEJKW.js → permissions-AHJWQPCL.js} +2 -2
  99. package/dist-cli/chunks/privacy-AYLEWFZX.js +4 -0
  100. package/dist-cli/chunks/{record-MA3AI752.js → record-V4MZ33XI.js} +3 -3
  101. package/dist-cli/chunks/{report-issue-4DTESWE2.js → report-issue-QBB54OJ5.js} +2 -2
  102. package/dist-cli/chunks/reset-BMGMOHRM.js +4 -0
  103. package/dist-cli/chunks/runtime-SYUM3ASK.js +4 -0
  104. package/dist-cli/chunks/{screenshot-command-JUXMWXS5.js → screenshot-command-PK54GDUY.js} +3 -3
  105. package/dist-cli/chunks/{screenshot-layers-DJF2E4PE.js → screenshot-layers-AQZK3GRM.js} +2 -2
  106. package/dist-cli/chunks/{screenshots-capture-UGZU5GOW.js → screenshots-capture-KXQDIF2S.js} +2 -2
  107. package/dist-cli/chunks/{serve-H7GNKJSM.js → serve-YPNVL33J.js} +2 -2
  108. package/dist-cli/chunks/setup-FM3JVRMY.js +4 -0
  109. package/dist-cli/chunks/{skills-4JGPOSFE.js → skills-4CBIW5IP.js} +2 -2
  110. package/dist-cli/chunks/{state-DEAQFKVP.js → state-AWM3LRRR.js} +2 -2
  111. package/dist-cli/chunks/{storage-Y6F5NTGH.js → storage-S2LC3T7Q.js} +2 -2
  112. package/dist-cli/chunks/store-SZX3GXCL.js +4 -0
  113. package/dist-cli/chunks/telemetry-ANUHW6KD.js +4 -0
  114. package/dist-cli/chunks/{timeline-HOHYYDOC.js → timeline-4AWI6M2T.js} +2 -2
  115. package/dist-cli/chunks/{upgrade-QF5YHLSG.js → upgrade-WSYVILOP.js} +2 -2
  116. package/dist-cli/chunks/upload-U2KPYBGD.js +4 -0
  117. package/dist-cli/chunks/{version-PDCKYJLB.js → version-XE4ZTCON.js} +2 -2
  118. package/dist-cli/chunks/{web-C5EUAFFW.js → web-ZS6L2NLF.js} +2 -2
  119. package/dist-cli/chunks/welcome-UE273JKY.js +5 -0
  120. package/dist-cli/chunks/{what-happened-L56ZFIMJ.js → what-happened-6AHCKADV.js} +2 -2
  121. package/dist-lib/agent-daemon-client.cjs +1 -1
  122. package/dist-lib/agent-events.cjs +1 -1
  123. package/dist-lib/agent-identity.cjs +1 -1
  124. package/dist-lib/agent-sessions.cjs +1 -1
  125. package/dist-lib/attached-projects.cjs +1 -1
  126. package/dist-lib/auth/shared-session.cjs +1 -1
  127. package/dist-lib/backend-origin.cjs +1 -1
  128. package/dist-lib/beta.cjs +1 -1
  129. package/dist-lib/beta.mjs +1 -1
  130. package/dist-lib/bridge-constants.cjs +1 -1
  131. package/dist-lib/bridge-contract.cjs +1 -1
  132. package/dist-lib/cli-constants.cjs +1 -1
  133. package/dist-lib/config.cjs +1 -1
  134. package/dist-lib/detox/index.cjs +196 -54
  135. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  136. package/dist-lib/home-paths.cjs +1 -1
  137. package/dist-lib/host/bridge-host.cjs +1 -1
  138. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  139. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  140. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  141. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  142. package/dist-lib/host/websocket-proxy.cjs +1 -1
  143. package/dist-lib/index.cjs +10 -3
  144. package/dist-lib/jump-to-source-babel.cjs +1 -1
  145. package/dist-lib/menu.cjs +1 -1
  146. package/dist-lib/menu.mjs +1 -1
  147. package/dist-lib/metro-production-bundle.cjs +1 -1
  148. package/dist-lib/metro-production-bundle.mjs +1 -1
  149. package/dist-lib/metro.cjs +1 -1
  150. package/dist-lib/profiles.cjs +1 -1
  151. package/dist-lib/public-brand.cjs +1 -1
  152. package/dist-lib/react-native-host-modules.cjs +1 -1
  153. package/dist-lib/react-native-host-modules.mjs +1 -1
  154. package/dist-lib/render-mode.cjs +1 -1
  155. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  156. package/dist-lib/sdk.cjs +10 -3
  157. package/dist-lib/sdk.mjs +10 -3
  158. package/dist-lib/skills.cjs +4778 -211
  159. package/dist-lib/vite.cjs +1 -1
  160. package/package.json +1 -1
  161. package/skills/rnx-test/SKILL.md +6 -2
  162. package/src/bridge-contract.ts +8 -2
  163. package/dist-cli/chunks/chunk-BAWV2UEF.js +0 -543
  164. package/dist-cli/chunks/chunk-YATZ7U4S.js +0 -4
  165. package/dist-cli/chunks/cli-version-VSDCFXXN.js +0 -4
  166. package/dist-cli/chunks/daemon-DLLEODJY.js +0 -4
  167. package/dist-cli/chunks/drivers-BB3BRKDS.js +0 -4
  168. package/dist-cli/chunks/flow-ALMSRBVL.js +0 -4
  169. package/dist-cli/chunks/help-3ZL7MRUX.js +0 -4
  170. package/dist-cli/chunks/inspect-RCU7XVAO.js +0 -4
  171. package/dist-cli/chunks/install-desktop-LYVKLTCU.js +0 -4
  172. package/dist-cli/chunks/privacy-SIDMP5EF.js +0 -4
  173. package/dist-cli/chunks/reset-GPHG5XN3.js +0 -4
  174. package/dist-cli/chunks/runtime-VOEIOODA.js +0 -4
  175. package/dist-cli/chunks/setup-G2WYLPM6.js +0 -4
  176. package/dist-cli/chunks/store-3EQT7VU6.js +0 -4
  177. package/dist-cli/chunks/telemetry-FANCQHKG.js +0 -4
  178. package/dist-cli/chunks/upload-QKJHFAX7.js +0 -4
  179. package/dist-cli/chunks/welcome-3CQ2GE4F.js +0 -5
@@ -2,8 +2,11 @@ import * as fs from 'fs'
2
2
  import * as path from 'path'
3
3
  import { devices, type DeviceModel } from 'sootsim-engine/settings'
4
4
  import yaml from 'yaml'
5
+ import {
6
+ REQUIRED_IDENTICAL_PROOF_FRAMES,
7
+ framesIdentical,
8
+ } from '../detox/proof-frame.cjs'
5
9
  import { composeFramedScreenshot } from '../src/screenshots/frame-compose'
6
- import { resetGuestAppStateEval } from './app-state-reset'
7
10
  import {
8
11
  READY_NODE_STABLE_MS,
9
12
  READY_PROBE_POLL_MS,
@@ -19,6 +22,10 @@ import { MaestroJsContext, scriptConditionIsTruthy } from './maestro-js'
19
22
  import { ensureCliRecordingEntitlement } from './recording-access'
20
23
  import type { PerformResult, PerformStep, ResetResult } from '../src/bridge-contract'
21
24
  import type { WsBridge } from './ws-bridge'
25
+ import type {
26
+ SootSimExternalAppLifecycleOptions,
27
+ SootSimLaunchArguments,
28
+ } from '@sootsim/globals'
22
29
 
23
30
  const DEFAULT_TIMEOUT = 10000
24
31
  // hosted-runner previews reach first-render much slower than local dev: the
@@ -407,6 +414,8 @@ export class SootSimBridgeFlowRunner {
407
414
  })
408
415
 
409
416
  private firstLaunchDone = false
417
+ private appStopped = false
418
+ private launchArgumentsActive = false
410
419
  private profilingEnabled = false
411
420
  private recordingEnabled = false
412
421
  private recordingAccessChecked = false
@@ -415,7 +424,6 @@ export class SootSimBridgeFlowRunner {
415
424
  private lastRecordingDurationMs: number | null = null
416
425
  private lastRecordingFrameStats: unknown = null
417
426
  private flowTraceSteps: SootSimFlowTraceStep[] = []
418
- private lastVisualSettledAtMs = 0
419
427
  private simRouteHint: string | null = null
420
428
  // set by runStep when a step throws — lets the outer flow command
421
429
  // dump a rich per-step failure bundle (screenshot + describe.json +
@@ -1489,23 +1497,6 @@ export class SootSimBridgeFlowRunner {
1489
1497
  layers?: 'full' | 'tenant' | 'shell'
1490
1498
  },
1491
1499
  ): Promise<string> {
1492
- // give transitions + last-moment data paint a brief settle window before
1493
- // freezing the frame. screenshot flows care more about visual completeness
1494
- // than minimum latency. waitForVisualSettle drains in-flight animations
1495
- // (it returns immediately if nothing is animating), then we hold for an
1496
- // additional 250ms paint window for fade-ins / image decodes / etc.
1497
- // before this change, a 250ms sleep was the *only* settle window — flows
1498
- // that took a screenshot at the tail of a screen transition would catch
1499
- // a half-finished frame on slow machines.
1500
- if (!this.hasFreshVisualSettle()) {
1501
- try {
1502
- await this.waitForVisualSettle()
1503
- await sleep(250)
1504
- } catch {
1505
- // settling helper can throw on disconnected sims; fall through so a
1506
- // stale sim still produces *some* screenshot.
1507
- }
1508
- }
1509
1500
  // flow `takeScreenshot: <name>` may be a plain name ("hero") or a full
1510
1501
  // relative path with slashes ("apps/foo/screenshots/en/02-forum") —
1511
1502
  // maestro flows commonly use the latter. mkdir the parent of the
@@ -1531,9 +1522,32 @@ export class SootSimBridgeFlowRunner {
1531
1522
  simId: this.opts.simId,
1532
1523
  }
1533
1524
  if (layers && layers !== 'full') screenshotRequest.layers = layers
1534
- const dataUrl: string = await this.bridge.send(screenshotRequest)
1535
- const base64 = dataUrl.replace(/^data:image\/png;base64,/, '')
1536
- const rawBuffer = Buffer.from(base64, 'base64')
1525
+ let previousBuffer: Buffer | null = null
1526
+ let identicalFrames = 1
1527
+ let rawBuffer: Buffer | null = null
1528
+ for (let attempt = 0; attempt < 12; attempt++) {
1529
+ await this.waitForVisualSettle()
1530
+ const dataUrl: string = await this.bridge.send(screenshotRequest)
1531
+ const current = Buffer.from(
1532
+ dataUrl.replace(/^data:image\/png;base64,/, ''),
1533
+ 'base64',
1534
+ )
1535
+ if (framesIdentical(previousBuffer, current)) {
1536
+ identicalFrames++
1537
+ if (identicalFrames >= REQUIRED_IDENTICAL_PROOF_FRAMES) {
1538
+ rawBuffer = current
1539
+ break
1540
+ }
1541
+ } else {
1542
+ identicalFrames = 1
1543
+ }
1544
+ previousBuffer = current
1545
+ }
1546
+ if (!rawBuffer) {
1547
+ throw new Error(
1548
+ `screenshot did not reach ${REQUIRED_IDENTICAL_PROOF_FRAMES} identical idle frames`,
1549
+ )
1550
+ }
1537
1551
  if (spec.withFrame) {
1538
1552
  const model = await this.readCurrentDeviceModel()
1539
1553
  if (!model) {
@@ -1563,13 +1577,7 @@ export class SootSimBridgeFlowRunner {
1563
1577
  }
1564
1578
 
1565
1579
  async captureScreenshot(outputPath: string) {
1566
- fs.mkdirSync(path.dirname(outputPath), { recursive: true })
1567
- const dataUrl: string = await this.bridge.send({
1568
- type: 'screenshot',
1569
- simId: this.opts.simId,
1570
- })
1571
- const base64 = dataUrl.replace(/^data:image\/png;base64,/, '')
1572
- fs.writeFileSync(outputPath, Buffer.from(base64, 'base64'))
1580
+ await this.takeScreenshot({ path: outputPath })
1573
1581
  }
1574
1582
 
1575
1583
  // writes a per-failure debug bundle: screenshot + structured describe
@@ -1880,7 +1888,9 @@ export class SootSimBridgeFlowRunner {
1880
1888
  stablePolls: 2,
1881
1889
  strict: true,
1882
1890
  })
1883
- if (result.settled) this.markVisualSettled()
1891
+ if (!result.settled) {
1892
+ throw new Error(`animation did not settle within ${maxMs}ms`)
1893
+ }
1884
1894
  }
1885
1895
 
1886
1896
  async back() {
@@ -1962,12 +1972,17 @@ export class SootSimBridgeFlowRunner {
1962
1972
  await this.refreshSimId()
1963
1973
  }
1964
1974
 
1965
- private async resetGuestAppData() {
1975
+ private async resetGuestAppData(
1976
+ launchOptions?: Pick<
1977
+ SootSimExternalAppLifecycleOptions,
1978
+ 'initialUrl' | 'launchArguments'
1979
+ >,
1980
+ ) {
1966
1981
  const reset: ResetResult = await this.bridge.send(
1967
1982
  {
1968
1983
  type: 'reset',
1969
1984
  simId: this.opts.simId,
1970
- resetOptions: { strategy: 'data' },
1985
+ resetOptions: { strategy: 'data', ...launchOptions },
1971
1986
  },
1972
1987
  { timeoutMs: 120000 },
1973
1988
  )
@@ -1982,14 +1997,16 @@ export class SootSimBridgeFlowRunner {
1982
1997
  }
1983
1998
  }
1984
1999
 
1985
- private async softReloadGuestApp(resetStorage: boolean) {
1986
- const reloaded = await this.evaluate<boolean>(
1987
- resetGuestAppStateEval(resetStorage),
1988
- 120000,
2000
+ private async reloadGuestAppRuntime(options: SootSimExternalAppLifecycleOptions) {
2001
+ await this.bridge.send(
2002
+ {
2003
+ type: 'call',
2004
+ simId: this.opts.simId,
2005
+ path: 'SootSim.bridges.hotRemount.reloadExternalApp',
2006
+ args: [options],
2007
+ },
2008
+ { timeoutMs: 120000 },
1989
2009
  )
1990
- if (!reloaded) {
1991
- throw new Error('guest app soft reload bridge unavailable')
1992
- }
1993
2010
  }
1994
2011
 
1995
2012
  private async rearmCaptureAfterRuntimeReload() {
@@ -2016,12 +2033,14 @@ export class SootSimBridgeFlowRunner {
2016
2033
 
2017
2034
  async launchApp(opts: any) {
2018
2035
  await this.refreshSimId()
2019
- if (opts && typeof opts === 'object' && opts.arguments) {
2020
- // real maestro passes process launch arguments to the native app.
2021
- // sootsim has no native process — the bundle boots in-page — so
2022
- // launch arguments have nothing to attach to. say so instead of
2023
- // silently dropping them.
2024
- console.log('[flow] launchApp.arguments ignored (rnx has no native process)')
2036
+ let hasLaunchArguments = false
2037
+ let launchArguments: SootSimLaunchArguments | undefined
2038
+ if (isObjectRecord(opts) && Object.prototype.hasOwnProperty.call(opts, 'arguments')) {
2039
+ if (!isObjectRecord(opts.arguments)) {
2040
+ throw new Error('launchApp.arguments must be a JSON object')
2041
+ }
2042
+ hasLaunchArguments = true
2043
+ launchArguments = opts.arguments
2025
2044
  }
2026
2045
  const wantsClear = !!(opts && typeof opts === 'object' && opts.clearState)
2027
2046
  const wantsResetRuntime = readBooleanKey(opts, 'resetRuntime')
@@ -2031,7 +2050,15 @@ export class SootSimBridgeFlowRunner {
2031
2050
  // Maestro's "launch the app" boundary through the shell. Otherwise the
2032
2051
  // guest app is already booting from the initial sim open, so just wait.
2033
2052
  this.firstLaunchDone = true
2034
- if (wantsResetRuntime) {
2053
+ if (hasLaunchArguments) {
2054
+ const launched = await this.launchShellAppFromHomeIfNeeded(opts)
2055
+ if (launched) await this.waitForTree(120000)
2056
+ await this.reloadGuestAppRuntime({
2057
+ launchArguments,
2058
+ initialUrl: null,
2059
+ })
2060
+ await this.waitForTree(120000)
2061
+ } else if (wantsResetRuntime) {
2035
2062
  // SootSim benchmark flows need app auth/storage kept warm while the
2036
2063
  // guest React runtime starts fresh inside the timed/recorded boundary.
2037
2064
  // the reload re-runs the app entry inside the mounted app surface, so
@@ -2045,7 +2072,14 @@ export class SootSimBridgeFlowRunner {
2045
2072
  // second readiness wait would only pad the measured reset.
2046
2073
  const launched = await this.launchShellAppFromHomeIfNeeded(opts)
2047
2074
  if (launched) await this.waitForTree(120000)
2048
- await this.softReloadGuestApp(false)
2075
+ await this.reloadGuestAppRuntime({})
2076
+ await this.waitForTree(120000)
2077
+ } else if (this.appStopped) {
2078
+ await this.launchShellAppFromHomeIfNeeded(opts)
2079
+ await this.reloadGuestAppRuntime({
2080
+ launchArguments: null,
2081
+ initialUrl: null,
2082
+ })
2049
2083
  await this.waitForTree(120000)
2050
2084
  } else {
2051
2085
  await this.launchShellAppFromHomeIfNeeded(opts)
@@ -2054,7 +2088,15 @@ export class SootSimBridgeFlowRunner {
2054
2088
  } else {
2055
2089
  this.firstLaunchDone = true
2056
2090
  if (wantsClear) {
2057
- await this.resetGuestAppData()
2091
+ await this.resetGuestAppData({
2092
+ launchArguments: hasLaunchArguments ? launchArguments : null,
2093
+ initialUrl: null,
2094
+ })
2095
+ } else if (hasLaunchArguments || this.launchArgumentsActive || this.appStopped) {
2096
+ await this.reloadGuestAppRuntime({
2097
+ launchArguments: hasLaunchArguments ? launchArguments : null,
2098
+ initialUrl: null,
2099
+ })
2058
2100
  } else {
2059
2101
  await this.reloadGuestApp()
2060
2102
  }
@@ -2072,6 +2114,9 @@ export class SootSimBridgeFlowRunner {
2072
2114
  }
2073
2115
  }
2074
2116
 
2117
+ this.appStopped = false
2118
+ this.launchArgumentsActive = hasLaunchArguments
2119
+
2075
2120
  if (this.profilingEnabled) {
2076
2121
  await this.startProfile()
2077
2122
  }
@@ -2082,31 +2127,19 @@ export class SootSimBridgeFlowRunner {
2082
2127
  }
2083
2128
 
2084
2129
  private async waitForVisualSettle() {
2085
- try {
2086
- await this.evaluate(`new Promise((resolve) => {
2087
- requestAnimationFrame(() => {
2088
- requestAnimationFrame(() => {
2089
- requestAnimationFrame(() => {
2090
- setTimeout(resolve, 180)
2091
- })
2092
- })
2093
- })
2094
- })`)
2095
- this.markVisualSettled()
2096
- } catch {
2097
- await sleep(180)
2098
- this.markVisualSettled()
2130
+ const result = await waitForSootsimIdle({
2131
+ bridge: this.bridge,
2132
+ simId: this.opts.simId,
2133
+ maxMs: 10_000,
2134
+ pollMs: 32,
2135
+ stablePolls: 3,
2136
+ strict: true,
2137
+ })
2138
+ if (!result.settled) {
2139
+ throw new Error(`visual state did not settle within ${result.elapsed}ms`)
2099
2140
  }
2100
2141
  }
2101
2142
 
2102
- private markVisualSettled() {
2103
- this.lastVisualSettledAtMs = Date.now()
2104
- }
2105
-
2106
- private hasFreshVisualSettle() {
2107
- return Date.now() - this.lastVisualSettledAtMs < 750
2108
- }
2109
-
2110
2143
  private resolveShellLaunchAppId(opts: unknown, state: ShellState | null): string {
2111
2144
  if (typeof opts === 'string' && opts.length > 0) return opts
2112
2145
  if (isObjectRecord(opts)) {
@@ -2188,12 +2221,10 @@ export class SootSimBridgeFlowRunner {
2188
2221
  }
2189
2222
 
2190
2223
  // maestro stopApp: tear down the current app runtime without clearing app
2191
- // data. sootsim has no separate native process, so a hard page reload is the
2192
- // nearest equivalent to killing and later re-launching the tenant runtime.
2193
- // State clearing belongs to clearState / launchApp({ clearState: true }).
2224
+ // data. the next launchApp or openLink creates its replacement worker.
2194
2225
  async stopApp() {
2195
- await this.reloadGuestApp()
2196
- await this.rearmCaptureAfterRuntimeReload()
2226
+ await this.reloadGuestAppRuntime({ relaunch: false })
2227
+ this.appStopped = true
2197
2228
  }
2198
2229
 
2199
2230
  // maestro clearState: wipe app-owned storage and relaunch fresh.
@@ -2265,6 +2296,17 @@ export class SootSimBridgeFlowRunner {
2265
2296
  async openLink(target: string | { link: string }) {
2266
2297
  const link = typeof target === 'string' ? target : target.link
2267
2298
  if (!link) throw new Error('openLink: missing link')
2299
+ if (this.appStopped) {
2300
+ await this.reloadGuestAppRuntime({
2301
+ initialUrl: link,
2302
+ launchArguments: null,
2303
+ })
2304
+ await this.waitForTree(120000)
2305
+ this.appStopped = false
2306
+ this.launchArgumentsActive = false
2307
+ this.firstLaunchDone = true
2308
+ return
2309
+ }
2268
2310
  const result = await this.callTest<{ ok: boolean; error?: string }>(
2269
2311
  'openDeepLink',
2270
2312
  link,
@@ -58,16 +58,21 @@ export async function waitForSootsimIdle({
58
58
 
59
59
  const readSnapshot = async () => {
60
60
  let animating = false
61
+ let layoutDirty = false
61
62
  let pendingFetches = 0
62
63
  let requestTotal = 0
63
64
  let requestInFlight = 0
65
+ let renderStatsAvailable = false
64
66
  try {
65
67
  const stats = await window.__sootsimRenderHost?.queryStats?.()
66
68
  if (stats) {
69
+ renderStatsAvailable = true
67
70
  animating =
68
71
  stats.hasActiveAnims === true ||
69
72
  stats.hasActiveNativeAnimations === true ||
70
73
  stats.hasPendingAnimationFrames === true
74
+ layoutDirty =
75
+ stats.layoutDirty === true && stats.renderRequested === true
71
76
  // a freshly-pushed screen showing a skeleton is layout-stable
72
77
  // but not actually settled — its data/images are still in
73
78
  // flight. treat bounded image-loader fetches as not-idle so
@@ -94,7 +99,7 @@ export async function waitForSootsimIdle({
94
99
  }
95
100
  walk(root)
96
101
  }
97
- return { layout: nodes.join(';'), animating, pendingFetches, requestTotal, requestInFlight }
102
+ return { layout: nodes.join(';'), animating, layoutDirty, pendingFetches, renderStatsAvailable, requestTotal, requestInFlight }
98
103
  }
99
104
 
100
105
  // how long generic background network is allowed to keep us waiting AFTER
@@ -114,7 +119,9 @@ export async function waitForSootsimIdle({
114
119
  let visuallyStillSince = 0
115
120
  while (Date.now() < deadline) {
116
121
  const snapshot = await readSnapshot()
117
- const strictOk = !strict || !snapshot.animating
122
+ const strictOk =
123
+ !strict ||
124
+ (snapshot.renderStatsAvailable && !snapshot.animating && !snapshot.layoutDirty)
118
125
  // visually settled: no animations, no image content loading, layout
119
126
  // unchanged since the last poll.
120
127
  const visuallyStill =
@@ -73,7 +73,7 @@ function printCompatMatrix() {
73
73
  rnx maestro — compatibility matrix
74
74
 
75
75
  supported verbs:
76
- launchApp, stopApp, clearState, clearKeychain (warn-only),
76
+ launchApp (including arguments), stopApp, clearState, clearKeychain (warn-only),
77
77
  tapOn, tapAtCoords, longPressOn,
78
78
  inputText, pressKey, dispatchKey, hideKeyboard, eraseText,
79
79
  assertVisible, assertNotVisible, assertTreeContains,
@@ -92,12 +92,17 @@ workspace mode (directory runs):
92
92
  config.yaml / config.yml discovery, flows: inclusion globs,
93
93
  includeTags / excludeTags against flow frontmatter tags.
94
94
 
95
+ launch behavior:
96
+ launchApp.arguments: a JSON object available before guest app entry through
97
+ react-native-launch-arguments and NativeModules.SettingsManager.settings.
98
+ stopApp: keeps the guest stopped until launchApp or openLink starts it.
99
+ openLink: cold-starts a stopped guest with the initial URL; a running guest
100
+ receives the URL through React Native Linking.
101
+
95
102
  partial:
96
103
  clearKeychain — warn-only, rnx has no keychain surface.
97
104
  when.platform — rnx reports iOS for this runner.
98
- openLink dispatched into the app via the Linking emulation
99
- (react-navigation deep links work); no OS-level Safari fallback.
100
- launchApp.arguments — ignored; rnx has no native process.
105
+ openLink: app routing works; no OS-level Safari fallback.
101
106
  launchApp.resetRuntime — sootsim-only extension for recorded benchmarks:
102
107
  keep tenant storage/auth, but soft-reload the guest app inside launchApp.
103
108
  takeScreenshot — maestro string form works unchanged; rnx also accepts
@@ -125,7 +125,7 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
125
125
  count: 3,
126
126
  },
127
127
  'packages/sootsim-engine/src/components/native-ui/LottieHost.tsx :: fetch :: descriptor.value':
128
- { category: 'guest_app_network', count: 1 },
128
+ { category: 'guest_app_network', count: 2 },
129
129
  'packages/sootsim-engine/src/demo/demo-share-register.ts :: fetch :: shareUrl': {
130
130
  category: 'guest_app_network',
131
131
  count: 1,