rnxsim 0.1.538 → 0.1.540
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/cloud-test-tunnel.ts +354 -0
- package/cli/commands/test.ts +333 -92
- package/cli/outbound-endpoints.ts +17 -1
- 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 +28 -2
- package/dist-lib/host/fetch-proxy-handler.cjs +28 -2
- package/dist-lib/host/fetch-proxy-overrides.cjs +33 -3
- package/dist-lib/host/fetch-proxy-overrides.mjs +30 -2
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +1 -1
- 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 +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +29 -3
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift.cjs +1 -1
- package/dist-lib/vite.cjs +1 -1
- package/package.json +1 -1
- package/src/host/fetch-proxy-handler.ts +2 -1
- package/src/host/fetch-proxy-overrides.ts +41 -0
package/cli/commands/test.ts
CHANGED
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
4
4
|
import { createServer, type Server } from 'node:http'
|
|
5
|
-
import { basename, dirname, extname, join, resolve } from 'node:path'
|
|
5
|
+
import { basename, dirname, extname, join, relative, resolve, sep } from 'node:path'
|
|
6
6
|
import {
|
|
7
7
|
RNX_CLOUD_TEST_FILES_MAX_BYTES,
|
|
8
8
|
RNX_CLOUD_TEST_FILES_MAX_COUNT,
|
|
9
9
|
} from '../../src/cloud-contract'
|
|
10
10
|
import { authHeaderValue, resolveCliAuth } from '../auth'
|
|
11
|
+
import {
|
|
12
|
+
parseCloudTestSidecarPorts,
|
|
13
|
+
parseLiveDevStackApp,
|
|
14
|
+
publishLiveDevStack,
|
|
15
|
+
} from '../cloud-test-tunnel'
|
|
16
|
+
import { parseFlowFile } from '../flow-file'
|
|
11
17
|
import { buildShellUrl, resolveShellBaseUrlForBridgePort } from './control'
|
|
12
18
|
import { hoistLeadingSimFlag } from './flow'
|
|
13
19
|
import { resolveDefaultUploadOrigin } from './upload'
|
|
@@ -48,6 +54,8 @@ const VALUE_FLAGS = new Set([
|
|
|
48
54
|
'--repo',
|
|
49
55
|
'--poll-interval',
|
|
50
56
|
'--timeout',
|
|
57
|
+
'--sidecar-ports',
|
|
58
|
+
'--cancel',
|
|
51
59
|
])
|
|
52
60
|
|
|
53
61
|
function valueAfter(args: string[], name: string): string | null {
|
|
@@ -202,6 +210,55 @@ export function resolveTestCommand(args: string[], cwd = process.cwd()) {
|
|
|
202
210
|
return { command: 'detox', args: forwarded }
|
|
203
211
|
}
|
|
204
212
|
|
|
213
|
+
// helper files a single flow pulls in through runFlow / runScript file
|
|
214
|
+
// refs, resolved relative to each referencing file exactly like the runner,
|
|
215
|
+
// transitively with a cycle guard. a ref that does not exist is the runner's
|
|
216
|
+
// loud failure, not the submitter's: warn and let the hosted run report it
|
|
217
|
+
// at its step, like a local run does.
|
|
218
|
+
function collectFlowHelperPaths(flowAbsPath: string): string[] {
|
|
219
|
+
if (!flowAbsPath.endsWith('.yml') && !flowAbsPath.endsWith('.yaml')) return []
|
|
220
|
+
const helpers: string[] = []
|
|
221
|
+
const visited = new Set<string>([resolve(flowAbsPath)])
|
|
222
|
+
const queue = [resolve(flowAbsPath)]
|
|
223
|
+
while (queue.length > 0) {
|
|
224
|
+
const current = queue.pop() as string
|
|
225
|
+
if (!current.endsWith('.yml') && !current.endsWith('.yaml')) continue
|
|
226
|
+
let steps: unknown[]
|
|
227
|
+
try {
|
|
228
|
+
steps = parseFlowFile(readFileSync(current, 'utf8')).steps
|
|
229
|
+
} catch (err) {
|
|
230
|
+
console.warn(
|
|
231
|
+
` warn: could not parse flow refs in ${current}: ${err instanceof Error ? err.message : String(err)}`,
|
|
232
|
+
)
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
for (const step of steps) {
|
|
236
|
+
if (typeof step !== 'object' || step === null) continue
|
|
237
|
+
for (const key of ['runFlow', 'runScript'] as const) {
|
|
238
|
+
const spec = (step as Record<string, unknown>)[key]
|
|
239
|
+
if (spec === undefined || spec === null) continue
|
|
240
|
+
const file =
|
|
241
|
+
typeof spec === 'string'
|
|
242
|
+
? spec
|
|
243
|
+
: typeof (spec as Record<string, unknown>).file === 'string'
|
|
244
|
+
? ((spec as Record<string, unknown>).file as string)
|
|
245
|
+
: null
|
|
246
|
+
if (!file) continue
|
|
247
|
+
const abs = resolve(dirname(current), file)
|
|
248
|
+
if (visited.has(abs)) continue
|
|
249
|
+
visited.add(abs)
|
|
250
|
+
if (existsSync(abs) && statSync(abs).isFile()) {
|
|
251
|
+
helpers.push(abs)
|
|
252
|
+
queue.push(abs)
|
|
253
|
+
} else {
|
|
254
|
+
console.warn(` warn: flow ref not found: ${file} (from ${current})`)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return helpers
|
|
260
|
+
}
|
|
261
|
+
|
|
205
262
|
function collectTestFiles(
|
|
206
263
|
targetPath: string,
|
|
207
264
|
ignoredPath: string | null,
|
|
@@ -240,8 +297,48 @@ function collectTestFiles(
|
|
|
240
297
|
}
|
|
241
298
|
walk(targetPath, '')
|
|
242
299
|
} else if (stat.isFile()) {
|
|
243
|
-
add(targetPath, basename(targetPath))
|
|
244
300
|
const parent = dirname(targetPath)
|
|
301
|
+
const helperAbsPaths = collectFlowHelperPaths(targetPath)
|
|
302
|
+
if (helperAbsPaths.length === 0) {
|
|
303
|
+
add(targetPath, basename(targetPath))
|
|
304
|
+
} else {
|
|
305
|
+
// the flow references helpers by path relative to itself (runFlow /
|
|
306
|
+
// runScript file). key every file by its path under their common root
|
|
307
|
+
// so the recorder's unpack preserves those relative refs.
|
|
308
|
+
const all = [resolve(targetPath), ...helperAbsPaths]
|
|
309
|
+
const split = all.map((abs) => abs.split(sep))
|
|
310
|
+
const commonParts = split[0]?.slice(0) ?? []
|
|
311
|
+
while (
|
|
312
|
+
commonParts.length > 0 &&
|
|
313
|
+
!split.every(
|
|
314
|
+
(parts) =>
|
|
315
|
+
parts.length >= commonParts.length &&
|
|
316
|
+
commonParts.every((part, i) => parts[i] === part),
|
|
317
|
+
)
|
|
318
|
+
) {
|
|
319
|
+
commonParts.pop()
|
|
320
|
+
}
|
|
321
|
+
const common = commonParts.join(sep) || sep
|
|
322
|
+
const keys = all.map((abs) => relative(common, abs).split(sep).join('/'))
|
|
323
|
+
for (const [index, abs] of all.entries()) {
|
|
324
|
+
add(abs, keys[index] as string)
|
|
325
|
+
}
|
|
326
|
+
// the recorder runs an unpacked multi-file upload as a workspace,
|
|
327
|
+
// whose default glob only sees top-level flows: a nested entry would
|
|
328
|
+
// lose to a top-level helper. pin a config that selects the entry.
|
|
329
|
+
if (!keys.some((key) => /^config\.ya?ml$/.test(basename(key)))) {
|
|
330
|
+
if (Object.keys(files).length >= RNX_CLOUD_TEST_FILES_MAX_COUNT) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
`cloud test suites may contain at most ${RNX_CLOUD_TEST_FILES_MAX_COUNT} files`,
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
const entryKey = keys[0] as string
|
|
336
|
+
const glob = entryKey.replace(/[\\*?[\]{}()!+@]/g, '\\$&')
|
|
337
|
+
files['config.yaml'] =
|
|
338
|
+
'# synthesized by rnx test --cloud: run exactly the submitted entry flow.\n' +
|
|
339
|
+
`flows:\n - ${JSON.stringify(glob)}\n`
|
|
340
|
+
}
|
|
341
|
+
}
|
|
245
342
|
for (const configName of [
|
|
246
343
|
'rnx-detox.config.cjs',
|
|
247
344
|
'jest.config.cjs',
|
|
@@ -262,6 +359,82 @@ function collectTestFiles(
|
|
|
262
359
|
return files
|
|
263
360
|
}
|
|
264
361
|
|
|
362
|
+
async function readJobStatus(
|
|
363
|
+
endpoint: string,
|
|
364
|
+
authorization: string,
|
|
365
|
+
): Promise<string | null> {
|
|
366
|
+
try {
|
|
367
|
+
const res = await fetch(endpoint, { headers: { authorization } })
|
|
368
|
+
if (!res.ok) return null
|
|
369
|
+
const json = (await res.json()) as { status?: unknown } | null
|
|
370
|
+
return json && typeof json.status === 'string' ? json.status : null
|
|
371
|
+
} catch {
|
|
372
|
+
return null
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export async function cancelCloudTest(jobId: string, origin: string): Promise<number> {
|
|
377
|
+
const auth = resolveCliAuth()
|
|
378
|
+
if (!auth) {
|
|
379
|
+
console.error(' error: cancelling a cloud test requires `rnx login` or RNX_API_KEY')
|
|
380
|
+
return 1
|
|
381
|
+
}
|
|
382
|
+
const endpoint = `${origin.replace(/\/$/, '')}/api/sootsim/runner/jobs?id=${encodeURIComponent(jobId)}`
|
|
383
|
+
let response: Response
|
|
384
|
+
try {
|
|
385
|
+
response = await fetch(endpoint, {
|
|
386
|
+
method: 'DELETE',
|
|
387
|
+
headers: { authorization: authHeaderValue(auth) },
|
|
388
|
+
})
|
|
389
|
+
} catch (err) {
|
|
390
|
+
console.error(
|
|
391
|
+
` error: could not connect to ${endpoint}: ${err instanceof Error ? err.message : String(err)}`,
|
|
392
|
+
)
|
|
393
|
+
return 1
|
|
394
|
+
}
|
|
395
|
+
if (response.status === 404) {
|
|
396
|
+
console.error(` error: cloud test job not found: ${jobId}`)
|
|
397
|
+
return 1
|
|
398
|
+
}
|
|
399
|
+
if (!response.ok) {
|
|
400
|
+
const body = await response.text()
|
|
401
|
+
let message = body
|
|
402
|
+
try {
|
|
403
|
+
const json = JSON.parse(body)
|
|
404
|
+
if (json.message) message = json.message
|
|
405
|
+
} catch {}
|
|
406
|
+
console.error(` error: cancel failed (${response.status}): ${message}`)
|
|
407
|
+
return 1
|
|
408
|
+
}
|
|
409
|
+
let status: string | null = null
|
|
410
|
+
try {
|
|
411
|
+
const json = (await response.json()) as { status?: unknown } | null
|
|
412
|
+
if (json && typeof json.status === 'string') status = json.status
|
|
413
|
+
} catch {
|
|
414
|
+
// an older server answers 2xx with no cancel body; fall through to the
|
|
415
|
+
// read-back below rather than claim anything.
|
|
416
|
+
}
|
|
417
|
+
if (status === null) {
|
|
418
|
+
status = await readJobStatus(endpoint, authHeaderValue(auth))
|
|
419
|
+
if (status === 'queued' || status === 'running') {
|
|
420
|
+
console.error(
|
|
421
|
+
` error: the server left ${jobId} ${status}; cancel needs a newer job api`,
|
|
422
|
+
)
|
|
423
|
+
return 1
|
|
424
|
+
}
|
|
425
|
+
if (status === null) {
|
|
426
|
+
console.error(` error: cancel sent but the state of ${jobId} is unknown`)
|
|
427
|
+
return 1
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (status === 'cancelled') {
|
|
431
|
+
console.log(` cloud test job cancelled: ${jobId}`)
|
|
432
|
+
} else {
|
|
433
|
+
console.log(` cloud test job already ${status}: ${jobId}`)
|
|
434
|
+
}
|
|
435
|
+
return 0
|
|
436
|
+
}
|
|
437
|
+
|
|
265
438
|
export async function runCloudTest(
|
|
266
439
|
args: string[],
|
|
267
440
|
opts: RunTestOptions = {},
|
|
@@ -269,6 +442,14 @@ export async function runCloudTest(
|
|
|
269
442
|
const forwarded = args.filter((a) => a !== '--cloud' && a !== '--hosted')
|
|
270
443
|
const originFlag = valueAfter(forwarded, '--origin')
|
|
271
444
|
const origin = await resolveDefaultUploadOrigin(originFlag ?? undefined)
|
|
445
|
+
const cancelId = valueAfter(forwarded, '--cancel')
|
|
446
|
+
if (forwarded.includes('--cancel') || cancelId !== null) {
|
|
447
|
+
if (!cancelId) {
|
|
448
|
+
console.error(' usage: rnx test --cancel <job-id>')
|
|
449
|
+
return 1
|
|
450
|
+
}
|
|
451
|
+
return cancelCloudTest(cancelId, origin)
|
|
452
|
+
}
|
|
272
453
|
const app =
|
|
273
454
|
valueAfter(forwarded, '--app') ||
|
|
274
455
|
valueAfter(forwarded, '--bundle') ||
|
|
@@ -307,7 +488,7 @@ export async function runCloudTest(
|
|
|
307
488
|
|
|
308
489
|
if (!app) {
|
|
309
490
|
console.error(
|
|
310
|
-
' error: rnx test --cloud requires --app <bundle.js|https://bundle-url>',
|
|
491
|
+
' error: rnx test --cloud requires --app <bundle.js|https://bundle-url|http://localhost:port>',
|
|
311
492
|
)
|
|
312
493
|
return 1
|
|
313
494
|
}
|
|
@@ -336,18 +517,41 @@ export async function runCloudTest(
|
|
|
336
517
|
let bundleUrl: string | undefined
|
|
337
518
|
let bundle: string | undefined
|
|
338
519
|
let bundleShareId: string | undefined
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
520
|
+
let tunnelTargetUrl: string | undefined
|
|
521
|
+
let tunnelHostHeader: string | undefined
|
|
522
|
+
let tunnelJobToken: string | undefined
|
|
523
|
+
let tunnelSidecarPorts: string | undefined
|
|
524
|
+
let closeTunnel: (() => void) | null = null
|
|
525
|
+
|
|
526
|
+
const liveDevStack = parseLiveDevStackApp(app)
|
|
527
|
+
const sidecarFlag = valueAfter(forwarded, '--sidecar-ports')
|
|
528
|
+
let liveSidecarPorts: number[] | null = null
|
|
529
|
+
if (liveDevStack) {
|
|
530
|
+
const parsedSidecars = parseCloudTestSidecarPorts(sidecarFlag, liveDevStack.port)
|
|
531
|
+
if (!parsedSidecars.ok) {
|
|
532
|
+
console.error(` error: ${parsedSidecars.message}`)
|
|
533
|
+
return 1
|
|
534
|
+
}
|
|
535
|
+
liveSidecarPorts = parsedSidecars.ports
|
|
344
536
|
} else {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
537
|
+
if (sidecarFlag !== null) {
|
|
538
|
+
console.error(
|
|
539
|
+
' error: --sidecar-ports requires a live dev stack --app (http://localhost:<port>)',
|
|
540
|
+
)
|
|
348
541
|
return 1
|
|
349
542
|
}
|
|
350
|
-
|
|
543
|
+
if (app.startsWith('http://') || app.startsWith('https://')) {
|
|
544
|
+
bundleUrl = app
|
|
545
|
+
} else if (app.startsWith('srb_') || app.includes('/api/preview/share/bundle')) {
|
|
546
|
+
bundleShareId = app.replace(/^.*id=/, '')
|
|
547
|
+
} else {
|
|
548
|
+
const localBundle = resolve(process.cwd(), app)
|
|
549
|
+
if (!existsSync(localBundle) || !statSync(localBundle).isFile()) {
|
|
550
|
+
console.error(` error: --app bundle file not found: ${app}`)
|
|
551
|
+
return 1
|
|
552
|
+
}
|
|
553
|
+
bundle = readFileSync(localBundle, 'utf8')
|
|
554
|
+
}
|
|
351
555
|
}
|
|
352
556
|
|
|
353
557
|
const targetName = basename(targetPath)
|
|
@@ -370,6 +574,26 @@ export async function runCloudTest(
|
|
|
370
574
|
return 1
|
|
371
575
|
}
|
|
372
576
|
|
|
577
|
+
// the tunnel stays open from submit until the job reaches a terminal
|
|
578
|
+
// state; tearing it down early strands the recorder mid-run.
|
|
579
|
+
if (liveDevStack && liveSidecarPorts) {
|
|
580
|
+
try {
|
|
581
|
+
const published = await publishLiveDevStack({
|
|
582
|
+
devPort: liveDevStack.port,
|
|
583
|
+
sidecarPorts: liveSidecarPorts,
|
|
584
|
+
})
|
|
585
|
+
tunnelTargetUrl = published.targetUrl
|
|
586
|
+
tunnelHostHeader = published.hostHeader
|
|
587
|
+
tunnelJobToken = published.jobToken
|
|
588
|
+
tunnelSidecarPorts = published.sidecarPorts.join(',') || undefined
|
|
589
|
+
closeTunnel = published.close
|
|
590
|
+
console.log(` live dev stack published: ${published.targetUrl}`)
|
|
591
|
+
} catch (err) {
|
|
592
|
+
console.error(` error: ${err instanceof Error ? err.message : String(err)}`)
|
|
593
|
+
return 1
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
373
597
|
const submitPayload: Record<string, unknown> = {
|
|
374
598
|
kind: 'test',
|
|
375
599
|
testFiles,
|
|
@@ -377,109 +601,126 @@ export async function runCloudTest(
|
|
|
377
601
|
if (bundleUrl) submitPayload.bundleUrl = bundleUrl
|
|
378
602
|
if (bundle) submitPayload.bundle = bundle
|
|
379
603
|
if (bundleShareId) submitPayload.bundleShareId = bundleShareId
|
|
604
|
+
if (tunnelTargetUrl) submitPayload.targetUrl = tunnelTargetUrl
|
|
605
|
+
if (tunnelHostHeader) submitPayload.hostHeader = tunnelHostHeader
|
|
606
|
+
if (tunnelJobToken) submitPayload.jobToken = tunnelJobToken
|
|
607
|
+
if (tunnelSidecarPorts) submitPayload.sidecarPorts = tunnelSidecarPorts
|
|
380
608
|
if (owner) submitPayload.owner = owner
|
|
381
609
|
if (repo) submitPayload.repo = repo
|
|
382
610
|
|
|
383
|
-
const endpoint = `${origin.replace(/\/$/, '')}/api/sootsim/runner/jobs`
|
|
384
|
-
let response: Response
|
|
385
611
|
try {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
headers: {
|
|
389
|
-
'content-type': 'application/json',
|
|
390
|
-
authorization: authHeaderValue(auth),
|
|
391
|
-
},
|
|
392
|
-
body: JSON.stringify(submitPayload),
|
|
393
|
-
})
|
|
394
|
-
} catch (err) {
|
|
395
|
-
console.error(
|
|
396
|
-
` error: could not connect to ${endpoint}: ${err instanceof Error ? err.message : String(err)}`,
|
|
397
|
-
)
|
|
398
|
-
return 1
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
if (!response.ok) {
|
|
402
|
-
const body = await response.text()
|
|
403
|
-
let message = body
|
|
612
|
+
const endpoint = `${origin.replace(/\/$/, '')}/api/sootsim/runner/jobs`
|
|
613
|
+
let response: Response
|
|
404
614
|
try {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
const { id } = (await response.json()) as { id: string }
|
|
415
|
-
console.log(` cloud test job submitted: ${id}`)
|
|
416
|
-
|
|
417
|
-
const pollUrl = `${origin.replace(/\/$/, '')}/api/sootsim/runner/jobs?id=${encodeURIComponent(id)}`
|
|
418
|
-
const startedAt = Date.now()
|
|
419
|
-
|
|
420
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
421
|
-
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs))
|
|
422
|
-
let pollRes: Response
|
|
423
|
-
try {
|
|
424
|
-
pollRes = await fetch(pollUrl, {
|
|
425
|
-
headers: { authorization: authHeaderValue(auth) },
|
|
615
|
+
response = await fetch(endpoint, {
|
|
616
|
+
method: 'POST',
|
|
617
|
+
headers: {
|
|
618
|
+
'content-type': 'application/json',
|
|
619
|
+
authorization: authHeaderValue(auth),
|
|
620
|
+
},
|
|
621
|
+
body: JSON.stringify(submitPayload),
|
|
426
622
|
})
|
|
427
623
|
} catch (err) {
|
|
428
|
-
console.
|
|
429
|
-
`
|
|
624
|
+
console.error(
|
|
625
|
+
` error: could not connect to ${endpoint}: ${err instanceof Error ? err.message : String(err)}`,
|
|
430
626
|
)
|
|
431
|
-
|
|
432
|
-
}
|
|
433
|
-
if (!pollRes.ok) {
|
|
434
|
-
console.warn(` warn: polling error: HTTP ${pollRes.status}`)
|
|
435
|
-
continue
|
|
627
|
+
return 1
|
|
436
628
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
629
|
+
|
|
630
|
+
if (!response.ok) {
|
|
631
|
+
const body = await response.text()
|
|
632
|
+
let message = body
|
|
633
|
+
try {
|
|
634
|
+
const json = JSON.parse(body)
|
|
635
|
+
if (json.message) message = json.message
|
|
636
|
+
} catch {}
|
|
637
|
+
console.error(
|
|
638
|
+
` error: cloud test submission failed (${response.status}): ${message}`,
|
|
639
|
+
)
|
|
640
|
+
return 1
|
|
447
641
|
}
|
|
448
642
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
643
|
+
const { id } = (await response.json()) as { id: string }
|
|
644
|
+
console.log(` cloud test job submitted: ${id}`)
|
|
645
|
+
|
|
646
|
+
const pollUrl = `${origin.replace(/\/$/, '')}/api/sootsim/runner/jobs?id=${encodeURIComponent(id)}`
|
|
647
|
+
const startedAt = Date.now()
|
|
648
|
+
|
|
649
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
650
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs))
|
|
651
|
+
let pollRes: Response
|
|
652
|
+
try {
|
|
653
|
+
pollRes = await fetch(pollUrl, {
|
|
654
|
+
headers: { authorization: authHeaderValue(auth) },
|
|
655
|
+
})
|
|
656
|
+
} catch (err) {
|
|
657
|
+
console.warn(
|
|
658
|
+
` warn: error polling job: ${err instanceof Error ? err.message : String(err)}`,
|
|
462
659
|
)
|
|
660
|
+
continue
|
|
661
|
+
}
|
|
662
|
+
if (!pollRes.ok) {
|
|
663
|
+
console.warn(` warn: polling error: HTTP ${pollRes.status}`)
|
|
664
|
+
continue
|
|
463
665
|
}
|
|
464
|
-
|
|
465
|
-
|
|
666
|
+
const job = (await pollRes.json()) as {
|
|
667
|
+
id: string
|
|
668
|
+
status: string
|
|
669
|
+
result?: {
|
|
670
|
+
previewId?: string | null
|
|
671
|
+
previewUrl?: string | null
|
|
672
|
+
runId?: string | null
|
|
673
|
+
exitCode?: number | null
|
|
674
|
+
failureReason?: string | null
|
|
675
|
+
} | null
|
|
466
676
|
}
|
|
467
|
-
|
|
468
|
-
|
|
677
|
+
|
|
678
|
+
if (
|
|
679
|
+
job.status === 'done' ||
|
|
680
|
+
job.status === 'failed' ||
|
|
681
|
+
job.status === 'expired' ||
|
|
682
|
+
job.status === 'cancelled'
|
|
683
|
+
) {
|
|
684
|
+
const result = job.result
|
|
685
|
+
// a job the recorder failed after a passing suite (missing recording or
|
|
686
|
+
// registration) is still a failed run.
|
|
687
|
+
const exitCode =
|
|
688
|
+
job.status === 'done' ? (result?.exitCode ?? 0) : result?.exitCode || 1
|
|
689
|
+
console.log(` status: ${job.status}`)
|
|
690
|
+
console.log(` exit code: ${exitCode}`)
|
|
691
|
+
if (result?.previewUrl) {
|
|
692
|
+
console.log(` preview: ${result.previewUrl}`)
|
|
693
|
+
} else if (result?.previewId) {
|
|
694
|
+
console.log(
|
|
695
|
+
` preview: ${origin.replace(/\/$/, '')}/preview/${result.previewId}`,
|
|
696
|
+
)
|
|
697
|
+
}
|
|
698
|
+
if (result?.runId) {
|
|
699
|
+
console.log(` run: ${result.runId}`)
|
|
700
|
+
}
|
|
701
|
+
if (result?.failureReason) {
|
|
702
|
+
console.error(` failure: ${result.failureReason}`)
|
|
703
|
+
}
|
|
704
|
+
return exitCode
|
|
469
705
|
}
|
|
470
|
-
return exitCode
|
|
471
706
|
}
|
|
472
|
-
}
|
|
473
707
|
|
|
474
|
-
|
|
475
|
-
|
|
708
|
+
console.error(` error: cloud test timed out after ${Math.round(timeoutMs / 1000)}s`)
|
|
709
|
+
return 1
|
|
710
|
+
} finally {
|
|
711
|
+
closeTunnel?.()
|
|
712
|
+
}
|
|
476
713
|
}
|
|
477
714
|
|
|
478
715
|
export async function runTest(
|
|
479
716
|
args: string[],
|
|
480
717
|
opts: RunTestOptions = {},
|
|
481
718
|
): Promise<number> {
|
|
482
|
-
if (
|
|
719
|
+
if (
|
|
720
|
+
args.includes('--cloud') ||
|
|
721
|
+
args.includes('--hosted') ||
|
|
722
|
+
args.some((arg) => arg === '--cancel' || arg.startsWith('--cancel='))
|
|
723
|
+
) {
|
|
483
724
|
return runCloudTest(args, opts)
|
|
484
725
|
}
|
|
485
726
|
|
|
@@ -133,6 +133,13 @@ export const OUTBOUND_ENDPOINTS: Record<string, OutboundEndpointDeclaration> = {
|
|
|
133
133
|
payload:
|
|
134
134
|
'the Metro JavaScript bundle selected by rnx ios --remote, then structured simulator commands and lifecycle requests',
|
|
135
135
|
},
|
|
136
|
+
tunnel_tool_download: {
|
|
137
|
+
category: 'explicit-user-action',
|
|
138
|
+
destination: 'https://github.com/cloudflare/cloudflared/releases/latest/download/',
|
|
139
|
+
preConsent: false,
|
|
140
|
+
payload:
|
|
141
|
+
'platform and architecture only, carried in the release asset name; fires only for rnx test --cloud with a live dev stack when no cloudflared binary is installed',
|
|
142
|
+
},
|
|
136
143
|
}
|
|
137
144
|
|
|
138
145
|
export interface OutboundCallDeclaration {
|
|
@@ -192,14 +199,23 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
|
|
|
192
199
|
category: 'guest_app_network',
|
|
193
200
|
count: 1,
|
|
194
201
|
},
|
|
202
|
+
// submitting a cloud test job (POST) and cancelling one (DELETE) share
|
|
203
|
+
// the jobs route: one destination, two call sites.
|
|
195
204
|
'packages/sootsim/cli/commands/test.ts :: fetch :: endpoint': {
|
|
196
205
|
category: 'contrast_user_action',
|
|
197
|
-
count:
|
|
206
|
+
count: 3,
|
|
198
207
|
},
|
|
199
208
|
'packages/sootsim/cli/commands/test.ts :: fetch :: pollUrl': {
|
|
200
209
|
category: 'contrast_user_action',
|
|
201
210
|
count: 1,
|
|
202
211
|
},
|
|
212
|
+
// the live-dev-stack publisher fetches a cloudflared binary only when none
|
|
213
|
+
// is installed; the token proxy below it forwards to the user's own
|
|
214
|
+
// loopback dev server and sidecars, never anywhere remote.
|
|
215
|
+
'packages/sootsim/cli/cloud-test-tunnel.ts :: fetch :: `https://github.com/cloudflare/cloudflared/releases/latest/download/${asset}`':
|
|
216
|
+
{ category: 'tunnel_tool_download', count: 1 },
|
|
217
|
+
"packages/sootsim/cli/cloud-test-tunnel.ts :: node-request :: { host: '127.0.0.1', port: selectedPort, path: req.url, method: req.method, headers, }":
|
|
218
|
+
{ category: 'local_development', count: 1 },
|
|
203
219
|
// compiling a project's .swift sources needs a swift toolchain, so the
|
|
204
220
|
// bundler posts them to the app's own /api/swift-compile. it only fires with
|
|
205
221
|
// a compile credential and only for a project that has .swift sources.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! rnx v0.1.
|
|
1
|
+
/*! rnx v0.1.540 | (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.540 | (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.540 | (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.540 | (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.540 | (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.540 | (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.540 | (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.540 | (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.540 | (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.540 | (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.540 | (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;
|