@essential-apps/shopify-test-runner 1.0.0
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/dist/contracts/normalize.d.ts +61 -0
- package/dist/contracts/normalize.d.ts.map +1 -0
- package/dist/contracts/normalize.js +99 -0
- package/dist/contracts/normalize.js.map +1 -0
- package/dist/contracts/normalizeHtml.d.ts +37 -0
- package/dist/contracts/normalizeHtml.d.ts.map +1 -0
- package/dist/contracts/normalizeHtml.js +89 -0
- package/dist/contracts/normalizeHtml.js.map +1 -0
- package/dist/edge/cert.d.ts +44 -0
- package/dist/edge/cert.d.ts.map +1 -0
- package/dist/edge/cert.js +117 -0
- package/dist/edge/cert.js.map +1 -0
- package/dist/edge/edgeProxy.d.ts +43 -0
- package/dist/edge/edgeProxy.d.ts.map +1 -0
- package/dist/edge/edgeProxy.js +297 -0
- package/dist/edge/edgeProxy.js.map +1 -0
- package/dist/edge/nodeShim.d.ts +2 -0
- package/dist/edge/nodeShim.d.ts.map +1 -0
- package/dist/edge/nodeShim.js +217 -0
- package/dist/edge/nodeShim.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/buildSourceBundle.d.ts +56 -0
- package/dist/lib/buildSourceBundle.d.ts.map +1 -0
- package/dist/lib/buildSourceBundle.js +153 -0
- package/dist/lib/buildSourceBundle.js.map +1 -0
- package/dist/lib/freePort.d.ts +5 -0
- package/dist/lib/freePort.d.ts.map +1 -0
- package/dist/lib/freePort.js +33 -0
- package/dist/lib/freePort.js.map +1 -0
- package/dist/lib/functionBuild.d.ts +99 -0
- package/dist/lib/functionBuild.d.ts.map +1 -0
- package/dist/lib/functionBuild.js +413 -0
- package/dist/lib/functionBuild.js.map +1 -0
- package/dist/lib/neonWsProxy.d.ts +41 -0
- package/dist/lib/neonWsProxy.d.ts.map +1 -0
- package/dist/lib/neonWsProxy.js +101 -0
- package/dist/lib/neonWsProxy.js.map +1 -0
- package/dist/lib/sourceZipUpload.d.ts +45 -0
- package/dist/lib/sourceZipUpload.d.ts.map +1 -0
- package/dist/lib/sourceZipUpload.js +129 -0
- package/dist/lib/sourceZipUpload.js.map +1 -0
- package/dist/lib/stealthLaunch.d.ts +35 -0
- package/dist/lib/stealthLaunch.d.ts.map +1 -0
- package/dist/lib/stealthLaunch.js +46 -0
- package/dist/lib/stealthLaunch.js.map +1 -0
- package/dist/lib/storeAutomation.d.ts +22 -0
- package/dist/lib/storeAutomation.d.ts.map +1 -0
- package/dist/lib/storeAutomation.js +85 -0
- package/dist/lib/storeAutomation.js.map +1 -0
- package/dist/playwright/baseConfig.d.ts +62 -0
- package/dist/playwright/baseConfig.d.ts.map +1 -0
- package/dist/playwright/baseConfig.js +68 -0
- package/dist/playwright/baseConfig.js.map +1 -0
- package/dist/playwright/globalSetup.d.ts +2 -0
- package/dist/playwright/globalSetup.d.ts.map +1 -0
- package/dist/playwright/globalSetup.js +139 -0
- package/dist/playwright/globalSetup.js.map +1 -0
- package/dist/playwright/index.d.ts +9 -0
- package/dist/playwright/index.d.ts.map +1 -0
- package/dist/playwright/index.js +9 -0
- package/dist/playwright/index.js.map +1 -0
- package/dist/probes/fonts.d.ts +4 -0
- package/dist/probes/fonts.d.ts.map +1 -0
- package/dist/probes/fonts.js +255 -0
- package/dist/probes/fonts.js.map +1 -0
- package/dist/probes/mirror.d.ts +4 -0
- package/dist/probes/mirror.d.ts.map +1 -0
- package/dist/probes/mirror.js +260 -0
- package/dist/probes/mirror.js.map +1 -0
- package/dist/probes/runProbe.d.ts +3 -0
- package/dist/probes/runProbe.d.ts.map +1 -0
- package/dist/probes/runProbe.js +219 -0
- package/dist/probes/runProbe.js.map +1 -0
- package/dist/probes/types.d.ts +72 -0
- package/dist/probes/types.d.ts.map +1 -0
- package/dist/probes/types.js +2 -0
- package/dist/probes/types.js.map +1 -0
- package/dist/scripts/_probeSourceUrl.d.ts +3 -0
- package/dist/scripts/_probeSourceUrl.d.ts.map +1 -0
- package/dist/scripts/_probeSourceUrl.js +119 -0
- package/dist/scripts/_probeSourceUrl.js.map +1 -0
- package/dist/scripts/addStore.d.ts +3 -0
- package/dist/scripts/addStore.d.ts.map +1 -0
- package/dist/scripts/addStore.js +46 -0
- package/dist/scripts/addStore.js.map +1 -0
- package/dist/scripts/buildDockerImage.d.ts +3 -0
- package/dist/scripts/buildDockerImage.d.ts.map +1 -0
- package/dist/scripts/buildDockerImage.js +60 -0
- package/dist/scripts/buildDockerImage.js.map +1 -0
- package/dist/scripts/captureAuth.d.ts +3 -0
- package/dist/scripts/captureAuth.d.ts.map +1 -0
- package/dist/scripts/captureAuth.js +124 -0
- package/dist/scripts/captureAuth.js.map +1 -0
- package/dist/scripts/captureContracts.d.ts +3 -0
- package/dist/scripts/captureContracts.d.ts.map +1 -0
- package/dist/scripts/captureContracts.js +517 -0
- package/dist/scripts/captureContracts.js.map +1 -0
- package/dist/scripts/captureRestContracts.d.ts +3 -0
- package/dist/scripts/captureRestContracts.d.ts.map +1 -0
- package/dist/scripts/captureRestContracts.js +245 -0
- package/dist/scripts/captureRestContracts.js.map +1 -0
- package/dist/scripts/checkOperationCoverage.d.ts +3 -0
- package/dist/scripts/checkOperationCoverage.d.ts.map +1 -0
- package/dist/scripts/checkOperationCoverage.js +302 -0
- package/dist/scripts/checkOperationCoverage.js.map +1 -0
- package/dist/scripts/cleanupStores.d.ts +3 -0
- package/dist/scripts/cleanupStores.d.ts.map +1 -0
- package/dist/scripts/cleanupStores.js +77 -0
- package/dist/scripts/cleanupStores.js.map +1 -0
- package/dist/scripts/createStores.d.ts +3 -0
- package/dist/scripts/createStores.d.ts.map +1 -0
- package/dist/scripts/createStores.js +66 -0
- package/dist/scripts/createStores.js.map +1 -0
- package/dist/scripts/deployAppVersion.d.ts +3 -0
- package/dist/scripts/deployAppVersion.d.ts.map +1 -0
- package/dist/scripts/deployAppVersion.js +591 -0
- package/dist/scripts/deployAppVersion.js.map +1 -0
- package/dist/scripts/devE2eBackend.d.ts +3 -0
- package/dist/scripts/devE2eBackend.d.ts.map +1 -0
- package/dist/scripts/devE2eBackend.js +117 -0
- package/dist/scripts/devE2eBackend.js.map +1 -0
- package/dist/scripts/devOnlineBackend.d.ts +3 -0
- package/dist/scripts/devOnlineBackend.d.ts.map +1 -0
- package/dist/scripts/devOnlineBackend.js +117 -0
- package/dist/scripts/devOnlineBackend.js.map +1 -0
- package/dist/scripts/installApp.d.ts +3 -0
- package/dist/scripts/installApp.d.ts.map +1 -0
- package/dist/scripts/installApp.js +163 -0
- package/dist/scripts/installApp.js.map +1 -0
- package/dist/scripts/listStores.d.ts +3 -0
- package/dist/scripts/listStores.d.ts.map +1 -0
- package/dist/scripts/listStores.js +18 -0
- package/dist/scripts/listStores.js.map +1 -0
- package/dist/scripts/runDocker.d.ts +3 -0
- package/dist/scripts/runDocker.d.ts.map +1 -0
- package/dist/scripts/runDocker.js +88 -0
- package/dist/scripts/runDocker.js.map +1 -0
- package/dist/scripts/runDockerAuth.d.ts +3 -0
- package/dist/scripts/runDockerAuth.d.ts.map +1 -0
- package/dist/scripts/runDockerAuth.js +108 -0
- package/dist/scripts/runDockerAuth.js.map +1 -0
- package/dist/scripts/runDockerOffline.d.ts +3 -0
- package/dist/scripts/runDockerOffline.d.ts.map +1 -0
- package/dist/scripts/runDockerOffline.js +129 -0
- package/dist/scripts/runDockerOffline.js.map +1 -0
- package/dist/scripts/runDockerOfflineExplore.d.ts +3 -0
- package/dist/scripts/runDockerOfflineExplore.d.ts.map +1 -0
- package/dist/scripts/runDockerOfflineExplore.js +116 -0
- package/dist/scripts/runDockerOfflineExplore.js.map +1 -0
- package/dist/scripts/runIsolatedDockerOffline.d.ts +3 -0
- package/dist/scripts/runIsolatedDockerOffline.d.ts.map +1 -0
- package/dist/scripts/runIsolatedDockerOffline.js +351 -0
- package/dist/scripts/runIsolatedDockerOffline.js.map +1 -0
- package/dist/scripts/runOffline.d.ts +3 -0
- package/dist/scripts/runOffline.d.ts.map +1 -0
- package/dist/scripts/runOffline.js +521 -0
- package/dist/scripts/runOffline.js.map +1 -0
- package/dist/scripts/runOfflineE2e.d.ts +3 -0
- package/dist/scripts/runOfflineE2e.d.ts.map +1 -0
- package/dist/scripts/runOfflineE2e.js +408 -0
- package/dist/scripts/runOfflineE2e.js.map +1 -0
- package/dist/scripts/runOfflineFullTests.d.ts +3 -0
- package/dist/scripts/runOfflineFullTests.d.ts.map +1 -0
- package/dist/scripts/runOfflineFullTests.js +1456 -0
- package/dist/scripts/runOfflineFullTests.js.map +1 -0
- package/dist/scripts/runSupermachine.d.ts +3 -0
- package/dist/scripts/runSupermachine.d.ts.map +1 -0
- package/dist/scripts/runSupermachine.js +474 -0
- package/dist/scripts/runSupermachine.js.map +1 -0
- package/dist/scripts/runSupermachineAuth.d.ts +3 -0
- package/dist/scripts/runSupermachineAuth.d.ts.map +1 -0
- package/dist/scripts/runSupermachineAuth.js +454 -0
- package/dist/scripts/runSupermachineAuth.js.map +1 -0
- package/dist/scripts/runTests.d.ts +3 -0
- package/dist/scripts/runTests.d.ts.map +1 -0
- package/dist/scripts/runTests.js +278 -0
- package/dist/scripts/runTests.js.map +1 -0
- package/dist/scripts/runVm.d.ts +3 -0
- package/dist/scripts/runVm.d.ts.map +1 -0
- package/dist/scripts/runVm.js +524 -0
- package/dist/scripts/runVm.js.map +1 -0
- package/dist/scripts/runVmAuth.d.ts +3 -0
- package/dist/scripts/runVmAuth.d.ts.map +1 -0
- package/dist/scripts/runVmAuth.js +475 -0
- package/dist/scripts/runVmAuth.js.map +1 -0
- package/dist/scripts/runVmScript.d.ts +3 -0
- package/dist/scripts/runVmScript.d.ts.map +1 -0
- package/dist/scripts/runVmScript.js +242 -0
- package/dist/scripts/runVmScript.js.map +1 -0
- package/dist/scripts/setupTestDb.d.ts +3 -0
- package/dist/scripts/setupTestDb.d.ts.map +1 -0
- package/dist/scripts/setupTestDb.js +61 -0
- package/dist/scripts/setupTestDb.js.map +1 -0
- package/dist/scripts/verifyContracts.d.ts +3 -0
- package/dist/scripts/verifyContracts.d.ts.map +1 -0
- package/dist/scripts/verifyContracts.js +258 -0
- package/dist/scripts/verifyContracts.js.map +1 -0
- package/dist/scripts/verifyRestContracts.d.ts +3 -0
- package/dist/scripts/verifyRestContracts.d.ts.map +1 -0
- package/dist/scripts/verifyRestContracts.js +237 -0
- package/dist/scripts/verifyRestContracts.js.map +1 -0
- package/dist/vite/offlineConfig.d.ts +34 -0
- package/dist/vite/offlineConfig.d.ts.map +1 -0
- package/dist/vite/offlineConfig.js +61 -0
- package/dist/vite/offlineConfig.js.map +1 -0
- package/dist/vite/onlineConfig.d.ts +42 -0
- package/dist/vite/onlineConfig.d.ts.map +1 -0
- package/dist/vite/onlineConfig.js +56 -0
- package/dist/vite/onlineConfig.js.map +1 -0
- package/docker/Dockerfile +67 -0
- package/docker/Dockerfile.vm +137 -0
- package/docker/README.md +50 -0
- package/docker/entrypoint.sh +198 -0
- package/package.json +85 -0
- package/src/contracts/normalize.ts +96 -0
- package/src/contracts/normalizeHtml.ts +98 -0
- package/src/edge/ca.cnf +14 -0
- package/src/edge/ca.crt +22 -0
- package/src/edge/ca.key +28 -0
- package/src/edge/cert.ts +117 -0
- package/src/edge/edgeProxy.ts +390 -0
- package/src/edge/server.cnf +28 -0
- package/src/edge/server.crt +26 -0
- package/src/edge/server.key +28 -0
- package/src/index.ts +67 -0
- package/src/lib/buildSourceBundle.ts +197 -0
- package/src/lib/freePort.ts +33 -0
- package/src/lib/functionBuild.ts +490 -0
- package/src/lib/neonWsProxy.ts +124 -0
- package/src/lib/sourceZipUpload.ts +168 -0
- package/src/lib/stealthLaunch.ts +57 -0
- package/src/lib/storeAutomation.ts +110 -0
- package/src/playwright/baseConfig.ts +120 -0
- package/src/playwright/globalSetup.ts +179 -0
- package/src/playwright/index.ts +11 -0
- package/src/probes/fonts.ts +279 -0
- package/src/probes/mirror.ts +283 -0
- package/src/probes/runProbe.ts +257 -0
- package/src/probes/types.ts +73 -0
- package/src/scripts/addStore.ts +59 -0
- package/src/scripts/buildDockerImage.ts +66 -0
- package/src/scripts/captureAuth.ts +145 -0
- package/src/scripts/captureContracts.ts +675 -0
- package/src/scripts/captureRestContracts.ts +319 -0
- package/src/scripts/checkOperationCoverage.ts +365 -0
- package/src/scripts/cleanupStores.ts +91 -0
- package/src/scripts/createStores.ts +77 -0
- package/src/scripts/deployAppVersion.ts +692 -0
- package/src/scripts/devOnlineBackend.ts +141 -0
- package/src/scripts/installApp.ts +188 -0
- package/src/scripts/listStores.ts +19 -0
- package/src/scripts/runDockerAuth.ts +120 -0
- package/src/scripts/runOffline.ts +577 -0
- package/src/scripts/runOfflineFullTests.ts +1634 -0
- package/src/scripts/runTests.ts +306 -0
- package/src/scripts/runVm.ts +562 -0
- package/src/scripts/runVmAuth.ts +541 -0
- package/src/scripts/runVmScript.ts +282 -0
- package/src/scripts/setupTestDb.ts +71 -0
- package/src/scripts/verifyContracts.ts +310 -0
- package/src/scripts/verifyRestContracts.ts +275 -0
- package/src/vite/onlineConfig.ts +60 -0
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* One-time interactive Shopify auth capture inside a VM.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists: online tests run inside an HVF arm64
|
|
6
|
+
* microVM (via supermachine HVF) with chromium under a virtual Xvfb display. There's no way
|
|
7
|
+
* to interactively log in to Shopify from outside the VM, but the
|
|
8
|
+
* captured storage state (cookies — Partners session AND, crucially,
|
|
9
|
+
* Cloudflare's `cf_clearance` Turnstile-bypass cookie) needs to be
|
|
10
|
+
* captured INSIDE that same VM so the browser fingerprint matches at
|
|
11
|
+
* test time. Captures from the macOS host (different UA, different
|
|
12
|
+
* canvas/WebGL hashes) trigger CF re-challenge.
|
|
13
|
+
*
|
|
14
|
+
* Flow:
|
|
15
|
+
* 1. Bake / acquire the same VM the online suite uses (same image,
|
|
16
|
+
* same warmupTag → reuses the snapshot — warm acquires are
|
|
17
|
+
* ~3 s instead of ~4 min cold bake).
|
|
18
|
+
* 2. Mount the workspace so storageState.json writes land on the
|
|
19
|
+
* host filesystem.
|
|
20
|
+
* 3. Start x11vnc inside the guest, bound to 0.0.0.0:5900.
|
|
21
|
+
* 4. Forward host 127.0.0.1:5900 → guest:5900 via vm.exposeTcp.
|
|
22
|
+
* 5. Open macOS Screen Sharing pointed at vnc://localhost:5900.
|
|
23
|
+
* 6. Spawn captureAuth.js inside the guest. It opens chrome on
|
|
24
|
+
* DISPLAY=:99, navigates to accounts.shopify.com/lookup, and
|
|
25
|
+
* waits for a host-side Enter keystroke.
|
|
26
|
+
* 7. User logs in via VNC, ALSO visits the test store admin to
|
|
27
|
+
* pass any Turnstile (so cf_clearance gets captured), then
|
|
28
|
+
* hits Enter in the host terminal.
|
|
29
|
+
* 8. captureAuth writes storageState.json (via the mounted
|
|
30
|
+
* workspace → host disk), then exits. We release the VM.
|
|
31
|
+
*
|
|
32
|
+
* Usage (one-time per developer machine, or when cf_clearance / the
|
|
33
|
+
* Partner session expires):
|
|
34
|
+
*
|
|
35
|
+
* npm run test:online:auth
|
|
36
|
+
*
|
|
37
|
+
* macOS Screen Sharing opens automatically. If it doesn't, run
|
|
38
|
+
* `open vnc://localhost:5900` manually. VNC password is "test" by
|
|
39
|
+
* default; override with TEST_ONLINE_VNC_PASSWORD.
|
|
40
|
+
*
|
|
41
|
+
* Supersedes runDockerAuth.ts (libkrun-based; we no longer use
|
|
42
|
+
* libkrun for any test path).
|
|
43
|
+
*/
|
|
44
|
+
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
45
|
+
import { homedir, platform as osPlatform } from 'node:os';
|
|
46
|
+
import { resolve } from 'node:path';
|
|
47
|
+
import { spawn as nodeSpawn } from 'node:child_process';
|
|
48
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
49
|
+
import { prepareOciArchive, envFileArgs } from '@essential-apps/shopify-test-core';
|
|
50
|
+
|
|
51
|
+
const repoRoot = process.cwd();
|
|
52
|
+
const VNC_PORT = 5900;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Resolve the VM image source the same way runVm.ts
|
|
56
|
+
* does. Without this, the VM runtime treats TEST_ONLINE_VM_IMAGE as
|
|
57
|
+
* a Docker Hub registry ref and tries to pull it — which 401s for
|
|
58
|
+
* locally-built `container build` images that never went to a
|
|
59
|
+
* registry. oci-archive mode is the only one that works for the
|
|
60
|
+
* Apple-`container`-built images we actually use.
|
|
61
|
+
*/
|
|
62
|
+
async function imageSourceOptions(): Promise<Record<string, unknown>> {
|
|
63
|
+
const mode = process.env['TEST_ONLINE_VM_IMAGE_SOURCE'] ?? 'oci-archive';
|
|
64
|
+
if (mode === 'registry') return {};
|
|
65
|
+
const ref =
|
|
66
|
+
process.env['TEST_ONLINE_VM_IMAGE'] ??
|
|
67
|
+
'essential-apps/shopify-test-vm:latest';
|
|
68
|
+
if (mode === 'oci-archive') {
|
|
69
|
+
const prep = await prepareOciArchive(ref);
|
|
70
|
+
if (prep.freshlySaved) {
|
|
71
|
+
console.error(
|
|
72
|
+
`[runVmAuth] saved ${ref} → ${prep.archivePath} (${(prep.sizeBytes / 1024 / 1024).toFixed(1)} MB)`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return { source: 'oci-archive', sourcePath: prep.archivePath };
|
|
76
|
+
}
|
|
77
|
+
if (mode === 'oci-layout') {
|
|
78
|
+
const sourcePath = process.env['TEST_ONLINE_VM_IMAGE_LAYOUT_PATH'];
|
|
79
|
+
if (!sourcePath) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`TEST_ONLINE_VM_IMAGE_SOURCE=oci-layout requires TEST_ONLINE_VM_IMAGE_LAYOUT_PATH`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return { source: 'oci-layout', sourcePath };
|
|
85
|
+
}
|
|
86
|
+
throw new Error(
|
|
87
|
+
`TEST_ONLINE_VM_IMAGE_SOURCE=${mode} not recognised. Valid: oci-archive | oci-layout | registry`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface PackageJson {
|
|
92
|
+
name?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function readAppName(): string {
|
|
96
|
+
const pkgPath = resolve(repoRoot, 'package.json');
|
|
97
|
+
if (!existsSync(pkgPath)) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`No package.json at ${pkgPath}. Run this from the consuming app's repo root.`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as PackageJson;
|
|
103
|
+
if (!pkg.name) throw new Error(`package.json at ${pkgPath} has no \`name\` field.`);
|
|
104
|
+
return pkg.name.replace(/^@[^/]+\//, '');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function main(): Promise<void> {
|
|
108
|
+
const appName = readAppName();
|
|
109
|
+
// Same node_modules volume + tarball-manifest extraFile as
|
|
110
|
+
// runVm.ts. Sharing both means auth-capture and online
|
|
111
|
+
// tests use the SAME snapshot (same warmupTag), so a single bake
|
|
112
|
+
// serves both flows.
|
|
113
|
+
const linuxModulesVolume =
|
|
114
|
+
process.env['TEST_LINUX_NODE_MODULES_VOLUME'] ??
|
|
115
|
+
resolve(homedir(), `.cache/${appName}-test/node_modules.img`);
|
|
116
|
+
mkdirSync(resolve(linuxModulesVolume, '..'), { recursive: true });
|
|
117
|
+
const tarballManifest = resolve(
|
|
118
|
+
repoRoot,
|
|
119
|
+
'vendor/essential-apps-shopify-test/.tarball-manifest',
|
|
120
|
+
);
|
|
121
|
+
const tarballManifestExists = existsSync(tarballManifest);
|
|
122
|
+
|
|
123
|
+
const vncPassword = process.env['TEST_ONLINE_VNC_PASSWORD'] ?? 'test';
|
|
124
|
+
const ref =
|
|
125
|
+
process.env['TEST_ONLINE_VM_IMAGE'] ?? 'essential-apps/shopify-test-vm:latest';
|
|
126
|
+
// amd64+Rosetta is the only path that has real Google Chrome
|
|
127
|
+
// installed (the Dockerfile installs google-chrome-stable amd64);
|
|
128
|
+
// we want real Chrome here so the captured fingerprint matches
|
|
129
|
+
// what online tests use (channel: 'chrome' via TEST_FORCE_REAL_CHROME,
|
|
130
|
+
// OR patchright bundled chromium — both run on this image).
|
|
131
|
+
const platform = process.env['TEST_ONLINE_VM_PLATFORM'] ?? 'linux/arm64';
|
|
132
|
+
|
|
133
|
+
console.error(`[runVmAuth] baking image…`);
|
|
134
|
+
const tBake = performance.now();
|
|
135
|
+
const { Image } = await import('@supermachine/core');
|
|
136
|
+
const imageOpts = await imageSourceOptions();
|
|
137
|
+
// Same warmup pattern as runVm.ts so this bake hits the
|
|
138
|
+
// existing snapshot (warmupTag must match). If runVm has
|
|
139
|
+
// never run cold yet, this will trigger that warmup — ~4 min once.
|
|
140
|
+
const image = await Image.build({
|
|
141
|
+
ref,
|
|
142
|
+
memoryMib: Number(process.env['TEST_ONLINE_VM_MEMORY_MIB'] ?? 8192),
|
|
143
|
+
vcpus: Number(process.env['TEST_ONLINE_VM_VCPUS'] ?? 4),
|
|
144
|
+
cmd: ['sleep', 'infinity'],
|
|
145
|
+
env: { SKIP_OFFLINE_HOST_HIJACK: '1' },
|
|
146
|
+
platform,
|
|
147
|
+
mounts: [
|
|
148
|
+
// VM runtime 0.7.28+ requires explicit guestPath and
|
|
149
|
+
// auto-mounts at boot before warmup fires.
|
|
150
|
+
{ hostPath: repoRoot, guestTag: 'workspace', guestPath: '/workspace' },
|
|
151
|
+
],
|
|
152
|
+
volumes: [
|
|
153
|
+
{
|
|
154
|
+
hostPath: linuxModulesVolume,
|
|
155
|
+
guestPath: '/workspace/node_modules',
|
|
156
|
+
sizeMib: 4096,
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
extraFiles: tarballManifestExists
|
|
160
|
+
? [
|
|
161
|
+
{ hostPath: tarballManifest, guestPath: '/etc/.shopify-test-tarball-manifest' },
|
|
162
|
+
]
|
|
163
|
+
: [],
|
|
164
|
+
// Independent auth-capture snapshot. (Despite the historical name
|
|
165
|
+
// this no longer shares runVm.ts's tag — that moved to
|
|
166
|
+
// `online-v15-<runtime>-<manifest>`.) This rarely-run flow keys on a
|
|
167
|
+
// static tag rather than auto-folding the @supermachine/core runtime
|
|
168
|
+
// version like the main runners, so bump it on warmup-script OR VM
|
|
169
|
+
// runtime changes. Bumped v3→v4 for the 0.7.62 runtime adoption.
|
|
170
|
+
warmupTag: 'online-v4',
|
|
171
|
+
// @ts-expect-error — @supermachine/core's BuildOptions type
|
|
172
|
+
// doesn't declare `warmup`, but the runtime accepts it. Tracked
|
|
173
|
+
// upstream; fix-or-augment when the types catch up.
|
|
174
|
+
warmup: async (vm: {
|
|
175
|
+
exec: (opts: { argv: string[]; timeoutMs?: number }) => Promise<{
|
|
176
|
+
exitCode: number;
|
|
177
|
+
stdout: Buffer;
|
|
178
|
+
stderr: Buffer;
|
|
179
|
+
}>;
|
|
180
|
+
}) => {
|
|
181
|
+
const r = await vm.exec({
|
|
182
|
+
argv: [
|
|
183
|
+
'sh',
|
|
184
|
+
'-c',
|
|
185
|
+
`
|
|
186
|
+
echo "[warmup] >>> ENTERED warmup callback at \$(date +%H:%M:%S.%N)"
|
|
187
|
+
set -e
|
|
188
|
+
# VM runtime 0.7.28+ auto-mounts /workspace +
|
|
189
|
+
# /workspace/node_modules BEFORE warmup fires. Keep the
|
|
190
|
+
# defence-in-depth virtiofs check so a future auto-mount
|
|
191
|
+
# regression can't let rm-rf nuke the host bind.
|
|
192
|
+
NM_FS=\$(stat -f -c %T /workspace/node_modules 2>/dev/null || stat --file-system --format=%T /workspace/node_modules)
|
|
193
|
+
echo "[warmup] /workspace/node_modules FS type: \$NM_FS"
|
|
194
|
+
if [ "\$NM_FS" = "virtiofs" ] || [ "\$NM_FS" = "fuse.virtiofs" ]; then
|
|
195
|
+
echo "FATAL: /workspace/node_modules is \$NM_FS — auto-mount broken"
|
|
196
|
+
grep ' /workspace' /proc/mounts
|
|
197
|
+
exit 1
|
|
198
|
+
fi
|
|
199
|
+
chown -R postgres:postgres /var/lib/postgresql/data
|
|
200
|
+
chmod 700 /var/lib/postgresql/data
|
|
201
|
+
mkdir -p /var/run/postgresql
|
|
202
|
+
chown postgres:postgres /var/run/postgresql
|
|
203
|
+
chmod 775 /var/run/postgresql
|
|
204
|
+
cd /workspace
|
|
205
|
+
# Fresh install every rebake (tarball-manifest hash
|
|
206
|
+
# invalidates the snapshot when tarballs change). Wiping
|
|
207
|
+
# before install ensures stale tarballs can't linger.
|
|
208
|
+
rm -rf node_modules/* node_modules/.[!.]* 2>/dev/null || true
|
|
209
|
+
# Don't pipe npm to "tail" — the pipeline status is tail's,
|
|
210
|
+
# masking npm failures under "set -e", so a transient install
|
|
211
|
+
# error would bake a half-installed node_modules cache that
|
|
212
|
+
# warm restores then reuse. Check npm's own status so a failed
|
|
213
|
+
# install aborts the bake (no cache poisoned, next run rebakes).
|
|
214
|
+
if ! npm install --legacy-peer-deps --engine-strict=false > /tmp/npm-install.log 2>&1; then
|
|
215
|
+
echo "--- npm install FAILED - last 40 lines ---"
|
|
216
|
+
tail -40 /tmp/npm-install.log
|
|
217
|
+
exit 1
|
|
218
|
+
fi
|
|
219
|
+
tail -30 /tmp/npm-install.log
|
|
220
|
+
if ! grep -q "^127.0.0.1.*localhost" /etc/hosts; then
|
|
221
|
+
echo "127.0.0.1 localhost" >> /etc/hosts
|
|
222
|
+
echo "::1 localhost" >> /etc/hosts
|
|
223
|
+
fi
|
|
224
|
+
`,
|
|
225
|
+
],
|
|
226
|
+
timeoutMs: 15 * 60 * 1000,
|
|
227
|
+
});
|
|
228
|
+
if (r.exitCode !== 0) {
|
|
229
|
+
console.error('[runVmAuth.warmup] failed:', r.stdout.toString(), r.stderr.toString());
|
|
230
|
+
throw new Error(`warmup failed (exit ${r.exitCode})`);
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
...imageOpts,
|
|
234
|
+
});
|
|
235
|
+
console.error(
|
|
236
|
+
`[runVmAuth] bake: ${((performance.now() - tBake) / 1000).toFixed(1)} s`,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const pool = await image.pool({ min: 1, max: 1, restoreOnRelease: false });
|
|
240
|
+
const vm = await pool.acquire();
|
|
241
|
+
|
|
242
|
+
// Loose typing so we can call signal/writeStdin/readStdout/etc.
|
|
243
|
+
// without re-importing the full @supermachine/core types tree (vendor name).
|
|
244
|
+
let forwarder: { stop: () => Promise<void> } | null = null;
|
|
245
|
+
let captureChild:
|
|
246
|
+
| {
|
|
247
|
+
signal: (n: number) => Promise<void>;
|
|
248
|
+
writeStdin: (b: Buffer) => Promise<void>;
|
|
249
|
+
readStdout?: () => Promise<Buffer>;
|
|
250
|
+
readStderr?: () => Promise<Buffer>;
|
|
251
|
+
wait?: () => Promise<{ exitCode: number }>;
|
|
252
|
+
}
|
|
253
|
+
| null = null;
|
|
254
|
+
|
|
255
|
+
// Idempotent cleanup. Two design constraints:
|
|
256
|
+
// 1. SIGINT may fire multiple times (user hammers Ctrl-C), and
|
|
257
|
+
// cleanup() is async — a second invocation would otherwise
|
|
258
|
+
// race the first, calling signal/wait on already-disposed
|
|
259
|
+
// handles ("agent closed connection before sending EXIT").
|
|
260
|
+
// 2. Each step may throw on second call (already-closed sockets,
|
|
261
|
+
// already-released vm, etc.) — wrap individually so a later
|
|
262
|
+
// step still runs even if an earlier one's idempotency check
|
|
263
|
+
// threw something the catch missed.
|
|
264
|
+
// Implementation: a singleton promise — first caller does the
|
|
265
|
+
// work, every subsequent caller awaits the same promise.
|
|
266
|
+
let cleanupPromise: Promise<void> | null = null;
|
|
267
|
+
const cleanup = (): Promise<void> => {
|
|
268
|
+
if (cleanupPromise) return cleanupPromise;
|
|
269
|
+
cleanupPromise = (async () => {
|
|
270
|
+
// Null each handle out after using it so a stray reference
|
|
271
|
+
// elsewhere can't be re-used.
|
|
272
|
+
const child = captureChild;
|
|
273
|
+
captureChild = null;
|
|
274
|
+
if (child) await child.signal(15).catch(() => undefined);
|
|
275
|
+
|
|
276
|
+
const fwd = forwarder;
|
|
277
|
+
forwarder = null;
|
|
278
|
+
if (fwd) await fwd.stop().catch(() => undefined);
|
|
279
|
+
|
|
280
|
+
await vm.release().catch(() => undefined);
|
|
281
|
+
await pool.shutdown().catch(() => undefined);
|
|
282
|
+
})();
|
|
283
|
+
return cleanupPromise;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// SIGINT / SIGTERM teardown — without these, Ctrl-C leaves the VM
|
|
287
|
+
// pinned and the next bake hits "snapshot in use" errors.
|
|
288
|
+
// First signal triggers cleanup + clean exit; second signal escalates
|
|
289
|
+
// to immediate process.exit so the user can always bail out.
|
|
290
|
+
let signalCount = 0;
|
|
291
|
+
const onSignal = (sig: NodeJS.Signals): void => {
|
|
292
|
+
signalCount += 1;
|
|
293
|
+
if (signalCount >= 2) {
|
|
294
|
+
console.error(`\n[runVmAuth] received ${sig} again — forcing exit`);
|
|
295
|
+
process.exit(130);
|
|
296
|
+
}
|
|
297
|
+
console.error(`\n[runVmAuth] received ${sig}, cleaning up…`);
|
|
298
|
+
cleanup()
|
|
299
|
+
.catch(() => undefined)
|
|
300
|
+
.finally(() => process.exit(130));
|
|
301
|
+
};
|
|
302
|
+
process.on('SIGINT', onSignal);
|
|
303
|
+
process.on('SIGTERM', onSignal);
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
// 0) Post-restore sanity. VM runtime 0.7.28+ captures the
|
|
307
|
+
// auto-mount tree in the snapshot, so /workspace and
|
|
308
|
+
// /workspace/node_modules are already mounted on restore.
|
|
309
|
+
// Assert + bail if anything's off.
|
|
310
|
+
const mountR = await vm.exec({
|
|
311
|
+
argv: ['sh', '-c', `
|
|
312
|
+
mountpoint -q /workspace || { echo "FATAL: /workspace not mounted"; exit 1; }
|
|
313
|
+
mountpoint -q /workspace/node_modules || { echo "FATAL: /workspace/node_modules not mounted"; exit 1; }
|
|
314
|
+
NM_FS=\$(stat -f -c %T /workspace/node_modules 2>/dev/null)
|
|
315
|
+
if [ "\$NM_FS" = "virtiofs" ] || [ "\$NM_FS" = "fuse.virtiofs" ]; then
|
|
316
|
+
echo "FATAL: /workspace/node_modules is \$NM_FS — auto-mount layering broken"
|
|
317
|
+
exit 1
|
|
318
|
+
fi
|
|
319
|
+
ls /workspace/node_modules/@essential-apps/shopify-test-runner/dist/scripts/captureAuth.js >/dev/null \
|
|
320
|
+
|| { echo "FATAL: captureAuth.js missing in volume — snapshot/volume out of sync"; exit 1; }
|
|
321
|
+
`],
|
|
322
|
+
timeoutMs: 10_000,
|
|
323
|
+
});
|
|
324
|
+
if (mountR.exitCode !== 0) {
|
|
325
|
+
console.error(`[runVmAuth] mount failed:\n${mountR.stdout.toString()}${mountR.stderr.toString()}`);
|
|
326
|
+
throw new Error('mount setup failed');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 1) Start x11vnc inside the guest. The image's entrypoint.sh
|
|
330
|
+
// has the same logic gated on TEST_ONLINE_VNC=1, but that env is
|
|
331
|
+
// bake-time only (already snapshotted). Running x11vnc here
|
|
332
|
+
// sidesteps the need for a separate baked snapshot.
|
|
333
|
+
console.error(`[runVmAuth] starting x11vnc on guest :5900…`);
|
|
334
|
+
const vncStart = await vm.exec({
|
|
335
|
+
argv: [
|
|
336
|
+
'bash',
|
|
337
|
+
'-c',
|
|
338
|
+
`
|
|
339
|
+
# No set -e — too many of these commands have valid nonzero
|
|
340
|
+
# exits (pkill matching 0 procs, rm-of-missing-file, etc.) and
|
|
341
|
+
# masking them all with || true was both noisy and unreliable.
|
|
342
|
+
# We explicitly check exit conditions on the things that
|
|
343
|
+
# matter (Xvfb socket present, x11vnc listening).
|
|
344
|
+
# Restart Xvfb on :99 with -ac (disable X access control)
|
|
345
|
+
# so x11vnc — which runs in a fresh shell here, separate from
|
|
346
|
+
# the entrypoint shell that started the original Xvfb — can
|
|
347
|
+
# attach without an Xauthority cookie. The entrypoint's Xvfb
|
|
348
|
+
# is fine for Playwright (which sets DISPLAY=:99 and inherits
|
|
349
|
+
# the auth context from the same shell), but x11vnc launched
|
|
350
|
+
# via a separate vm.exec doesn't have that.
|
|
351
|
+
echo "[xvfb] stopping any existing Xvfb on :99…"
|
|
352
|
+
# IMPORTANT: do NOT use 'pkill -f' here — the -f flag matches
|
|
353
|
+
# the full command line, and our own bash script's argv
|
|
354
|
+
# contains the literal string we'd search for (it's an
|
|
355
|
+
# embedded heredoc), so pkill would kill its own parent
|
|
356
|
+
# shell. Match by process name only.
|
|
357
|
+
pkill -x Xvfb 2>/dev/null || true
|
|
358
|
+
rm -f /tmp/.X11-unix/X99 /tmp/.X99-lock
|
|
359
|
+
sleep 0.3
|
|
360
|
+
# -ac: disable access control (no auth required). Safe inside
|
|
361
|
+
# the VM — only x11vnc here is going to connect, and x11vnc's
|
|
362
|
+
# own VNC auth (-rfbauth) gates external access.
|
|
363
|
+
# -nolisten tcp: still no TCP listener on Xvfb itself.
|
|
364
|
+
echo "[xvfb] starting Xvfb :99 -ac …"
|
|
365
|
+
nohup Xvfb :99 -screen 0 1600x1000x24 -nolisten tcp -ac \
|
|
366
|
+
>/var/log/xvfb.log 2>&1 &
|
|
367
|
+
for _ in $(seq 1 50); do
|
|
368
|
+
[ -e /tmp/.X11-unix/X99 ] && break
|
|
369
|
+
sleep 0.1
|
|
370
|
+
done
|
|
371
|
+
if [ ! -e /tmp/.X11-unix/X99 ]; then
|
|
372
|
+
echo "[xvfb] FAILED to bind /tmp/.X11-unix/X99 within 5s"
|
|
373
|
+
cat /var/log/xvfb.log
|
|
374
|
+
exit 1
|
|
375
|
+
fi
|
|
376
|
+
echo "[xvfb] up."
|
|
377
|
+
|
|
378
|
+
mkdir -p /root/.vnc
|
|
379
|
+
x11vnc -storepasswd "${vncPassword}" /root/.vnc/passwd >/dev/null 2>&1
|
|
380
|
+
# Background via shell (&) not x11vnc's -bg (which has been
|
|
381
|
+
# observed to fork before the accept loop binds). nohup so
|
|
382
|
+
# the listener survives this exec call returning.
|
|
383
|
+
echo "[x11vnc] starting on :${VNC_PORT}…"
|
|
384
|
+
nohup x11vnc -display :99 -forever -shared \
|
|
385
|
+
-rfbauth /root/.vnc/passwd -rfbport ${VNC_PORT} -quiet \
|
|
386
|
+
>/var/log/x11vnc.log 2>&1 &
|
|
387
|
+
VNC_PID=$!
|
|
388
|
+
echo "[x11vnc] pid $VNC_PID"
|
|
389
|
+
# Verify the listener actually bound. Use netstat (from
|
|
390
|
+
# net-tools — installed in both the vm and libkrun
|
|
391
|
+
# images per the Dockerfiles). bash /dev/tcp was tried first
|
|
392
|
+
# but ubuntu/jammy bash sometimes ships without the
|
|
393
|
+
# /dev/tcp/* virtual device enabled, leading to silent probe
|
|
394
|
+
# failures even when x11vnc IS listening.
|
|
395
|
+
for _ in $(seq 1 30); do
|
|
396
|
+
if netstat -ltn 2>/dev/null | grep -q ":${VNC_PORT} "; then
|
|
397
|
+
exit 0
|
|
398
|
+
fi
|
|
399
|
+
sleep 0.1
|
|
400
|
+
done
|
|
401
|
+
echo "x11vnc didn't bind :${VNC_PORT} within 3s"
|
|
402
|
+
echo "--- netstat output ---"
|
|
403
|
+
netstat -ltn 2>&1 || echo "(netstat unavailable)"
|
|
404
|
+
echo "--- x11vnc log ---"
|
|
405
|
+
cat /var/log/x11vnc.log
|
|
406
|
+
exit 1
|
|
407
|
+
`,
|
|
408
|
+
],
|
|
409
|
+
timeoutMs: 15_000,
|
|
410
|
+
});
|
|
411
|
+
if (vncStart.exitCode !== 0) {
|
|
412
|
+
console.error(
|
|
413
|
+
'[runVmAuth] x11vnc failed:',
|
|
414
|
+
vncStart.stdout.toString(),
|
|
415
|
+
vncStart.stderr.toString(),
|
|
416
|
+
);
|
|
417
|
+
throw new Error('x11vnc startup failed');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// 2) Forward host 127.0.0.1:5900 → guest :5900 so the macOS
|
|
421
|
+
// Screen Sharing client can reach the VNC server.
|
|
422
|
+
forwarder = await vm.exposeTcp(VNC_PORT, VNC_PORT);
|
|
423
|
+
console.error(
|
|
424
|
+
`[runVmAuth] forwarding host 127.0.0.1:${VNC_PORT} → guest :${VNC_PORT}`,
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
// 3) Open macOS Screen Sharing pointed at the forwarder. Detach
|
|
428
|
+
// so the parent process can carry on.
|
|
429
|
+
if (osPlatform() === 'darwin') {
|
|
430
|
+
console.error(
|
|
431
|
+
`[runVmAuth] VNC password (paste into Screen Sharing): ${vncPassword}`,
|
|
432
|
+
);
|
|
433
|
+
// Tiny delay so x11vnc accept-loop is warm before the client
|
|
434
|
+
// dials; otherwise Screen Sharing sometimes shows "connection
|
|
435
|
+
// refused" and the user has to retry.
|
|
436
|
+
await sleep(500);
|
|
437
|
+
// Embed the password in the URL (vnc://:<pw>@host) so macOS Screen
|
|
438
|
+
// Sharing connects WITHOUT prompting — prefilled, not typed. URL-encode
|
|
439
|
+
// in case TEST_ONLINE_VNC_PASSWORD has reserved chars.
|
|
440
|
+
const vncUrl = `vnc://:${encodeURIComponent(vncPassword)}@localhost:${VNC_PORT}`;
|
|
441
|
+
console.error(
|
|
442
|
+
`[runVmAuth] Opening macOS Screen Sharing → vnc://localhost:${VNC_PORT} (password prefilled)`,
|
|
443
|
+
);
|
|
444
|
+
console.error(`[runVmAuth] (If nothing opens: open '${vncUrl}')`);
|
|
445
|
+
nodeSpawn('open', [vncUrl], {
|
|
446
|
+
stdio: 'ignore',
|
|
447
|
+
detached: true,
|
|
448
|
+
}).unref();
|
|
449
|
+
} else {
|
|
450
|
+
console.error(
|
|
451
|
+
`[runVmAuth] Connect any VNC viewer to localhost:${VNC_PORT} (password: ${vncPassword}).`,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// 4) Spawn captureAuth.js inside the guest. It opens Chrome on
|
|
456
|
+
// DISPLAY=:99, prompts for Enter on stdin, then writes
|
|
457
|
+
// storageState.json (which lands on host disk via the
|
|
458
|
+
// workspace mount). We bridge host stdin to the guest child
|
|
459
|
+
// so the user's Enter keystroke reaches captureAuth's
|
|
460
|
+
// readline prompt.
|
|
461
|
+
console.error('');
|
|
462
|
+
console.error('━'.repeat(72));
|
|
463
|
+
console.error('Inside Screen Sharing:');
|
|
464
|
+
console.error(' 1. Log in to Shopify Partners (accounts.shopify.com/lookup).');
|
|
465
|
+
console.error(' 2. Then visit your test store admin');
|
|
466
|
+
console.error(' (e.g. https://admin.shopify.com/store/<your-store>).');
|
|
467
|
+
console.error(' If a Cloudflare "Verify you are human" page appears,');
|
|
468
|
+
console.error(' click through it. Without this step, cf_clearance');
|
|
469
|
+
console.error(' never lands in storageState and tests will still hit');
|
|
470
|
+
console.error(' Turnstile.');
|
|
471
|
+
console.error(' 3. Return here and press Enter to save & exit.');
|
|
472
|
+
console.error('━'.repeat(72));
|
|
473
|
+
console.error('');
|
|
474
|
+
|
|
475
|
+
captureChild = await vm.spawn({
|
|
476
|
+
argv: [
|
|
477
|
+
'sh',
|
|
478
|
+
'-c',
|
|
479
|
+
// tty=false here (no PTY) because we want the readline prompt
|
|
480
|
+
// to read from a plain stdin pipe — Chrome doesn't care, it
|
|
481
|
+
// reads DISPLAY from env.
|
|
482
|
+
`cd /workspace && DISPLAY=:99 TEST_IN_CONTAINER=true node ${envFileArgs(repoRoot)} node_modules/@essential-apps/shopify-test-runner/dist/scripts/captureAuth.js`,
|
|
483
|
+
],
|
|
484
|
+
env: { DISPLAY: ':99', TEST_IN_CONTAINER: 'true' },
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// Bridge host stdin → guest captureAuth stdin (for the Enter
|
|
488
|
+
// prompt). Forwarder runs until the guest child exits.
|
|
489
|
+
process.stdin.setEncoding('utf8');
|
|
490
|
+
process.stdin.on('data', (chunk: string | Buffer) => {
|
|
491
|
+
const buf = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk;
|
|
492
|
+
captureChild!.writeStdin(buf).catch(() => undefined);
|
|
493
|
+
});
|
|
494
|
+
process.stdin.resume();
|
|
495
|
+
|
|
496
|
+
// Stream guest stdout/stderr to host for visibility.
|
|
497
|
+
const streamGuest = async (
|
|
498
|
+
reader: (() => Promise<Buffer>) | undefined,
|
|
499
|
+
out: NodeJS.WriteStream,
|
|
500
|
+
): Promise<void> => {
|
|
501
|
+
if (!reader) return;
|
|
502
|
+
// eslint-disable-next-line no-constant-condition
|
|
503
|
+
while (true) {
|
|
504
|
+
const chunk = await reader().catch(() => null);
|
|
505
|
+
if (!chunk || chunk.length === 0) return;
|
|
506
|
+
out.write(chunk);
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
// captureChild has readStdout / readStderr — both Promise<Buffer>
|
|
510
|
+
// chunks until EOF (zero-length).
|
|
511
|
+
const childAny = captureChild as unknown as {
|
|
512
|
+
readStdout?: () => Promise<Buffer>;
|
|
513
|
+
readStderr?: () => Promise<Buffer>;
|
|
514
|
+
wait?: () => Promise<{ exitCode: number }>;
|
|
515
|
+
};
|
|
516
|
+
void streamGuest(childAny.readStdout, process.stdout);
|
|
517
|
+
void streamGuest(childAny.readStderr, process.stderr);
|
|
518
|
+
|
|
519
|
+
const result = await (childAny.wait?.() ?? Promise.resolve({ exitCode: 0 }));
|
|
520
|
+
if (result.exitCode === 0) {
|
|
521
|
+
console.error('');
|
|
522
|
+
console.error('[runVmAuth] ✓ captureAuth completed successfully.');
|
|
523
|
+
console.error('[runVmAuth] storageState.json written via workspace mount.');
|
|
524
|
+
} else {
|
|
525
|
+
console.error(
|
|
526
|
+
`[runVmAuth] captureAuth exited with code ${result.exitCode}`,
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
await cleanup();
|
|
530
|
+
process.exit(result.exitCode);
|
|
531
|
+
} catch (err) {
|
|
532
|
+
console.error('[runVmAuth] fatal:', (err as Error).message);
|
|
533
|
+
await cleanup();
|
|
534
|
+
process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
main().catch(async (err) => {
|
|
539
|
+
console.error(err);
|
|
540
|
+
process.exit(1);
|
|
541
|
+
});
|