@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.
Files changed (265) hide show
  1. package/dist/contracts/normalize.d.ts +61 -0
  2. package/dist/contracts/normalize.d.ts.map +1 -0
  3. package/dist/contracts/normalize.js +99 -0
  4. package/dist/contracts/normalize.js.map +1 -0
  5. package/dist/contracts/normalizeHtml.d.ts +37 -0
  6. package/dist/contracts/normalizeHtml.d.ts.map +1 -0
  7. package/dist/contracts/normalizeHtml.js +89 -0
  8. package/dist/contracts/normalizeHtml.js.map +1 -0
  9. package/dist/edge/cert.d.ts +44 -0
  10. package/dist/edge/cert.d.ts.map +1 -0
  11. package/dist/edge/cert.js +117 -0
  12. package/dist/edge/cert.js.map +1 -0
  13. package/dist/edge/edgeProxy.d.ts +43 -0
  14. package/dist/edge/edgeProxy.d.ts.map +1 -0
  15. package/dist/edge/edgeProxy.js +297 -0
  16. package/dist/edge/edgeProxy.js.map +1 -0
  17. package/dist/edge/nodeShim.d.ts +2 -0
  18. package/dist/edge/nodeShim.d.ts.map +1 -0
  19. package/dist/edge/nodeShim.js +217 -0
  20. package/dist/edge/nodeShim.js.map +1 -0
  21. package/dist/index.d.ts +39 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +36 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/lib/buildSourceBundle.d.ts +56 -0
  26. package/dist/lib/buildSourceBundle.d.ts.map +1 -0
  27. package/dist/lib/buildSourceBundle.js +153 -0
  28. package/dist/lib/buildSourceBundle.js.map +1 -0
  29. package/dist/lib/freePort.d.ts +5 -0
  30. package/dist/lib/freePort.d.ts.map +1 -0
  31. package/dist/lib/freePort.js +33 -0
  32. package/dist/lib/freePort.js.map +1 -0
  33. package/dist/lib/functionBuild.d.ts +99 -0
  34. package/dist/lib/functionBuild.d.ts.map +1 -0
  35. package/dist/lib/functionBuild.js +413 -0
  36. package/dist/lib/functionBuild.js.map +1 -0
  37. package/dist/lib/neonWsProxy.d.ts +41 -0
  38. package/dist/lib/neonWsProxy.d.ts.map +1 -0
  39. package/dist/lib/neonWsProxy.js +101 -0
  40. package/dist/lib/neonWsProxy.js.map +1 -0
  41. package/dist/lib/sourceZipUpload.d.ts +45 -0
  42. package/dist/lib/sourceZipUpload.d.ts.map +1 -0
  43. package/dist/lib/sourceZipUpload.js +129 -0
  44. package/dist/lib/sourceZipUpload.js.map +1 -0
  45. package/dist/lib/stealthLaunch.d.ts +35 -0
  46. package/dist/lib/stealthLaunch.d.ts.map +1 -0
  47. package/dist/lib/stealthLaunch.js +46 -0
  48. package/dist/lib/stealthLaunch.js.map +1 -0
  49. package/dist/lib/storeAutomation.d.ts +22 -0
  50. package/dist/lib/storeAutomation.d.ts.map +1 -0
  51. package/dist/lib/storeAutomation.js +85 -0
  52. package/dist/lib/storeAutomation.js.map +1 -0
  53. package/dist/playwright/baseConfig.d.ts +62 -0
  54. package/dist/playwright/baseConfig.d.ts.map +1 -0
  55. package/dist/playwright/baseConfig.js +68 -0
  56. package/dist/playwright/baseConfig.js.map +1 -0
  57. package/dist/playwright/globalSetup.d.ts +2 -0
  58. package/dist/playwright/globalSetup.d.ts.map +1 -0
  59. package/dist/playwright/globalSetup.js +139 -0
  60. package/dist/playwright/globalSetup.js.map +1 -0
  61. package/dist/playwright/index.d.ts +9 -0
  62. package/dist/playwright/index.d.ts.map +1 -0
  63. package/dist/playwright/index.js +9 -0
  64. package/dist/playwright/index.js.map +1 -0
  65. package/dist/probes/fonts.d.ts +4 -0
  66. package/dist/probes/fonts.d.ts.map +1 -0
  67. package/dist/probes/fonts.js +255 -0
  68. package/dist/probes/fonts.js.map +1 -0
  69. package/dist/probes/mirror.d.ts +4 -0
  70. package/dist/probes/mirror.d.ts.map +1 -0
  71. package/dist/probes/mirror.js +260 -0
  72. package/dist/probes/mirror.js.map +1 -0
  73. package/dist/probes/runProbe.d.ts +3 -0
  74. package/dist/probes/runProbe.d.ts.map +1 -0
  75. package/dist/probes/runProbe.js +219 -0
  76. package/dist/probes/runProbe.js.map +1 -0
  77. package/dist/probes/types.d.ts +72 -0
  78. package/dist/probes/types.d.ts.map +1 -0
  79. package/dist/probes/types.js +2 -0
  80. package/dist/probes/types.js.map +1 -0
  81. package/dist/scripts/_probeSourceUrl.d.ts +3 -0
  82. package/dist/scripts/_probeSourceUrl.d.ts.map +1 -0
  83. package/dist/scripts/_probeSourceUrl.js +119 -0
  84. package/dist/scripts/_probeSourceUrl.js.map +1 -0
  85. package/dist/scripts/addStore.d.ts +3 -0
  86. package/dist/scripts/addStore.d.ts.map +1 -0
  87. package/dist/scripts/addStore.js +46 -0
  88. package/dist/scripts/addStore.js.map +1 -0
  89. package/dist/scripts/buildDockerImage.d.ts +3 -0
  90. package/dist/scripts/buildDockerImage.d.ts.map +1 -0
  91. package/dist/scripts/buildDockerImage.js +60 -0
  92. package/dist/scripts/buildDockerImage.js.map +1 -0
  93. package/dist/scripts/captureAuth.d.ts +3 -0
  94. package/dist/scripts/captureAuth.d.ts.map +1 -0
  95. package/dist/scripts/captureAuth.js +124 -0
  96. package/dist/scripts/captureAuth.js.map +1 -0
  97. package/dist/scripts/captureContracts.d.ts +3 -0
  98. package/dist/scripts/captureContracts.d.ts.map +1 -0
  99. package/dist/scripts/captureContracts.js +517 -0
  100. package/dist/scripts/captureContracts.js.map +1 -0
  101. package/dist/scripts/captureRestContracts.d.ts +3 -0
  102. package/dist/scripts/captureRestContracts.d.ts.map +1 -0
  103. package/dist/scripts/captureRestContracts.js +245 -0
  104. package/dist/scripts/captureRestContracts.js.map +1 -0
  105. package/dist/scripts/checkOperationCoverage.d.ts +3 -0
  106. package/dist/scripts/checkOperationCoverage.d.ts.map +1 -0
  107. package/dist/scripts/checkOperationCoverage.js +302 -0
  108. package/dist/scripts/checkOperationCoverage.js.map +1 -0
  109. package/dist/scripts/cleanupStores.d.ts +3 -0
  110. package/dist/scripts/cleanupStores.d.ts.map +1 -0
  111. package/dist/scripts/cleanupStores.js +77 -0
  112. package/dist/scripts/cleanupStores.js.map +1 -0
  113. package/dist/scripts/createStores.d.ts +3 -0
  114. package/dist/scripts/createStores.d.ts.map +1 -0
  115. package/dist/scripts/createStores.js +66 -0
  116. package/dist/scripts/createStores.js.map +1 -0
  117. package/dist/scripts/deployAppVersion.d.ts +3 -0
  118. package/dist/scripts/deployAppVersion.d.ts.map +1 -0
  119. package/dist/scripts/deployAppVersion.js +591 -0
  120. package/dist/scripts/deployAppVersion.js.map +1 -0
  121. package/dist/scripts/devE2eBackend.d.ts +3 -0
  122. package/dist/scripts/devE2eBackend.d.ts.map +1 -0
  123. package/dist/scripts/devE2eBackend.js +117 -0
  124. package/dist/scripts/devE2eBackend.js.map +1 -0
  125. package/dist/scripts/devOnlineBackend.d.ts +3 -0
  126. package/dist/scripts/devOnlineBackend.d.ts.map +1 -0
  127. package/dist/scripts/devOnlineBackend.js +117 -0
  128. package/dist/scripts/devOnlineBackend.js.map +1 -0
  129. package/dist/scripts/installApp.d.ts +3 -0
  130. package/dist/scripts/installApp.d.ts.map +1 -0
  131. package/dist/scripts/installApp.js +163 -0
  132. package/dist/scripts/installApp.js.map +1 -0
  133. package/dist/scripts/listStores.d.ts +3 -0
  134. package/dist/scripts/listStores.d.ts.map +1 -0
  135. package/dist/scripts/listStores.js +18 -0
  136. package/dist/scripts/listStores.js.map +1 -0
  137. package/dist/scripts/runDocker.d.ts +3 -0
  138. package/dist/scripts/runDocker.d.ts.map +1 -0
  139. package/dist/scripts/runDocker.js +88 -0
  140. package/dist/scripts/runDocker.js.map +1 -0
  141. package/dist/scripts/runDockerAuth.d.ts +3 -0
  142. package/dist/scripts/runDockerAuth.d.ts.map +1 -0
  143. package/dist/scripts/runDockerAuth.js +108 -0
  144. package/dist/scripts/runDockerAuth.js.map +1 -0
  145. package/dist/scripts/runDockerOffline.d.ts +3 -0
  146. package/dist/scripts/runDockerOffline.d.ts.map +1 -0
  147. package/dist/scripts/runDockerOffline.js +129 -0
  148. package/dist/scripts/runDockerOffline.js.map +1 -0
  149. package/dist/scripts/runDockerOfflineExplore.d.ts +3 -0
  150. package/dist/scripts/runDockerOfflineExplore.d.ts.map +1 -0
  151. package/dist/scripts/runDockerOfflineExplore.js +116 -0
  152. package/dist/scripts/runDockerOfflineExplore.js.map +1 -0
  153. package/dist/scripts/runIsolatedDockerOffline.d.ts +3 -0
  154. package/dist/scripts/runIsolatedDockerOffline.d.ts.map +1 -0
  155. package/dist/scripts/runIsolatedDockerOffline.js +351 -0
  156. package/dist/scripts/runIsolatedDockerOffline.js.map +1 -0
  157. package/dist/scripts/runOffline.d.ts +3 -0
  158. package/dist/scripts/runOffline.d.ts.map +1 -0
  159. package/dist/scripts/runOffline.js +521 -0
  160. package/dist/scripts/runOffline.js.map +1 -0
  161. package/dist/scripts/runOfflineE2e.d.ts +3 -0
  162. package/dist/scripts/runOfflineE2e.d.ts.map +1 -0
  163. package/dist/scripts/runOfflineE2e.js +408 -0
  164. package/dist/scripts/runOfflineE2e.js.map +1 -0
  165. package/dist/scripts/runOfflineFullTests.d.ts +3 -0
  166. package/dist/scripts/runOfflineFullTests.d.ts.map +1 -0
  167. package/dist/scripts/runOfflineFullTests.js +1456 -0
  168. package/dist/scripts/runOfflineFullTests.js.map +1 -0
  169. package/dist/scripts/runSupermachine.d.ts +3 -0
  170. package/dist/scripts/runSupermachine.d.ts.map +1 -0
  171. package/dist/scripts/runSupermachine.js +474 -0
  172. package/dist/scripts/runSupermachine.js.map +1 -0
  173. package/dist/scripts/runSupermachineAuth.d.ts +3 -0
  174. package/dist/scripts/runSupermachineAuth.d.ts.map +1 -0
  175. package/dist/scripts/runSupermachineAuth.js +454 -0
  176. package/dist/scripts/runSupermachineAuth.js.map +1 -0
  177. package/dist/scripts/runTests.d.ts +3 -0
  178. package/dist/scripts/runTests.d.ts.map +1 -0
  179. package/dist/scripts/runTests.js +278 -0
  180. package/dist/scripts/runTests.js.map +1 -0
  181. package/dist/scripts/runVm.d.ts +3 -0
  182. package/dist/scripts/runVm.d.ts.map +1 -0
  183. package/dist/scripts/runVm.js +524 -0
  184. package/dist/scripts/runVm.js.map +1 -0
  185. package/dist/scripts/runVmAuth.d.ts +3 -0
  186. package/dist/scripts/runVmAuth.d.ts.map +1 -0
  187. package/dist/scripts/runVmAuth.js +475 -0
  188. package/dist/scripts/runVmAuth.js.map +1 -0
  189. package/dist/scripts/runVmScript.d.ts +3 -0
  190. package/dist/scripts/runVmScript.d.ts.map +1 -0
  191. package/dist/scripts/runVmScript.js +242 -0
  192. package/dist/scripts/runVmScript.js.map +1 -0
  193. package/dist/scripts/setupTestDb.d.ts +3 -0
  194. package/dist/scripts/setupTestDb.d.ts.map +1 -0
  195. package/dist/scripts/setupTestDb.js +61 -0
  196. package/dist/scripts/setupTestDb.js.map +1 -0
  197. package/dist/scripts/verifyContracts.d.ts +3 -0
  198. package/dist/scripts/verifyContracts.d.ts.map +1 -0
  199. package/dist/scripts/verifyContracts.js +258 -0
  200. package/dist/scripts/verifyContracts.js.map +1 -0
  201. package/dist/scripts/verifyRestContracts.d.ts +3 -0
  202. package/dist/scripts/verifyRestContracts.d.ts.map +1 -0
  203. package/dist/scripts/verifyRestContracts.js +237 -0
  204. package/dist/scripts/verifyRestContracts.js.map +1 -0
  205. package/dist/vite/offlineConfig.d.ts +34 -0
  206. package/dist/vite/offlineConfig.d.ts.map +1 -0
  207. package/dist/vite/offlineConfig.js +61 -0
  208. package/dist/vite/offlineConfig.js.map +1 -0
  209. package/dist/vite/onlineConfig.d.ts +42 -0
  210. package/dist/vite/onlineConfig.d.ts.map +1 -0
  211. package/dist/vite/onlineConfig.js +56 -0
  212. package/dist/vite/onlineConfig.js.map +1 -0
  213. package/docker/Dockerfile +67 -0
  214. package/docker/Dockerfile.vm +137 -0
  215. package/docker/README.md +50 -0
  216. package/docker/entrypoint.sh +198 -0
  217. package/package.json +85 -0
  218. package/src/contracts/normalize.ts +96 -0
  219. package/src/contracts/normalizeHtml.ts +98 -0
  220. package/src/edge/ca.cnf +14 -0
  221. package/src/edge/ca.crt +22 -0
  222. package/src/edge/ca.key +28 -0
  223. package/src/edge/cert.ts +117 -0
  224. package/src/edge/edgeProxy.ts +390 -0
  225. package/src/edge/server.cnf +28 -0
  226. package/src/edge/server.crt +26 -0
  227. package/src/edge/server.key +28 -0
  228. package/src/index.ts +67 -0
  229. package/src/lib/buildSourceBundle.ts +197 -0
  230. package/src/lib/freePort.ts +33 -0
  231. package/src/lib/functionBuild.ts +490 -0
  232. package/src/lib/neonWsProxy.ts +124 -0
  233. package/src/lib/sourceZipUpload.ts +168 -0
  234. package/src/lib/stealthLaunch.ts +57 -0
  235. package/src/lib/storeAutomation.ts +110 -0
  236. package/src/playwright/baseConfig.ts +120 -0
  237. package/src/playwright/globalSetup.ts +179 -0
  238. package/src/playwright/index.ts +11 -0
  239. package/src/probes/fonts.ts +279 -0
  240. package/src/probes/mirror.ts +283 -0
  241. package/src/probes/runProbe.ts +257 -0
  242. package/src/probes/types.ts +73 -0
  243. package/src/scripts/addStore.ts +59 -0
  244. package/src/scripts/buildDockerImage.ts +66 -0
  245. package/src/scripts/captureAuth.ts +145 -0
  246. package/src/scripts/captureContracts.ts +675 -0
  247. package/src/scripts/captureRestContracts.ts +319 -0
  248. package/src/scripts/checkOperationCoverage.ts +365 -0
  249. package/src/scripts/cleanupStores.ts +91 -0
  250. package/src/scripts/createStores.ts +77 -0
  251. package/src/scripts/deployAppVersion.ts +692 -0
  252. package/src/scripts/devOnlineBackend.ts +141 -0
  253. package/src/scripts/installApp.ts +188 -0
  254. package/src/scripts/listStores.ts +19 -0
  255. package/src/scripts/runDockerAuth.ts +120 -0
  256. package/src/scripts/runOffline.ts +577 -0
  257. package/src/scripts/runOfflineFullTests.ts +1634 -0
  258. package/src/scripts/runTests.ts +306 -0
  259. package/src/scripts/runVm.ts +562 -0
  260. package/src/scripts/runVmAuth.ts +541 -0
  261. package/src/scripts/runVmScript.ts +282 -0
  262. package/src/scripts/setupTestDb.ts +71 -0
  263. package/src/scripts/verifyContracts.ts +310 -0
  264. package/src/scripts/verifyRestContracts.ts +275 -0
  265. package/src/vite/onlineConfig.ts +60 -0
@@ -0,0 +1,562 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Run online tests inside an HVF microVM (via supermachine HVF).
4
+ *
5
+ * Drop-in replacement for `runDocker.ts`. Same workflow, same test
6
+ * outputs, but uses the VM runtime's arm64 microVM with virtio-fs DAX
7
+ * bind mounts instead of Apple `container`'s libkrun:
8
+ *
9
+ * libkrun vm
10
+ * ───────── ────────────
11
+ * amd64 image (Rosetta) arm64 image (native)
12
+ * ~10 s cold boot ~5 s cold bake / ~1 s warm restore
13
+ * ~30 s/test wall-clock ~3× faster end-to-end on offline-full
14
+ * Google Chrome amd64 in Xvfb Playwright bundled chromium in
15
+ * Xvfb (patchright stealth still works)
16
+ *
17
+ * Architecture:
18
+ * - Image: `essential-apps/shopify-test-vm:latest` baked
19
+ * via `tests/test-offline/docker/Dockerfile.vm` in the consuming
20
+ * app (the equivalent of `Dockerfile` for libkrun). Auto-saved as
21
+ * a local OCI archive on first invocation via `prepareOciArchive`.
22
+ * - Bind mounts (virtio-fs DAX):
23
+ * - consuming app repo → /workspace
24
+ * - linux node_modules cache → /workspace/node_modules
25
+ * Both writable in-VM, host changes reflected immediately.
26
+ * - Postgres autostarted in the warmup callback (folded into the
27
+ * snapshot so warm restores skip the cost).
28
+ * - `runTests.js` invoked inside the VM; its localhost:5432 points
29
+ * at the in-VM postgres started by the warmup.
30
+ *
31
+ * Override env vars (all match the conformance package's conventions
32
+ * so users only learn one set):
33
+ * TEST_ONLINE_VM_IMAGE ref to bake/restore. Default:
34
+ * `essential-apps/shopify-test-vm:latest`
35
+ * TEST_ONLINE_VM_IMAGE_SOURCE oci-archive | oci-layout | registry. Default: oci-archive.
36
+ * TEST_ONLINE_VM_IMAGE_LAYOUT_PATH required when source=oci-layout.
37
+ * TEST_ONLINE_VM_MEMORY_MIB guest RAM. Default: 8192.
38
+ * TEST_ONLINE_VM_VCPUS guest vCPUs. Default: 4.
39
+ * TEST_LINUX_NODE_MODULES_VOLUME host path for the Linux node_modules
40
+ * sparse-file volume.
41
+ * Default: ~/.cache/${appName}-test/node_modules.img
42
+ * TEST_ONLINE_VM_TIMEOUT_MS overall guard for runTests.js. Default: 10 min.
43
+ */
44
+ import { createHash } from 'node:crypto';
45
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
46
+ import { createRequire } from 'node:module';
47
+ import { homedir } from 'node:os';
48
+ import { resolve } from 'node:path';
49
+ import { prepareOciArchive, envFileArgs } from '@essential-apps/shopify-test-core';
50
+
51
+ const repoRoot = process.cwd();
52
+
53
+ interface PackageJson {
54
+ name?: string;
55
+ }
56
+
57
+ function readAppName(): string {
58
+ const pkgPath = resolve(repoRoot, 'package.json');
59
+ if (!existsSync(pkgPath)) {
60
+ throw new Error(
61
+ `No package.json at ${pkgPath}. Run this from the consuming app's repo root.`,
62
+ );
63
+ }
64
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as PackageJson;
65
+ if (!pkg.name) {
66
+ throw new Error(`package.json at ${pkgPath} has no \`name\` field.`);
67
+ }
68
+ return pkg.name.replace(/^@[^/]+\//, '');
69
+ }
70
+
71
+ /**
72
+ * Resolve the VM image source the same way the conformance
73
+ * runner does. Duplicated here intentionally — extracting into a
74
+ * shared helper would create a runner→conformance dep cycle (runner
75
+ * is allowed to depend on conformance, but in practice this is the
76
+ * single use-site and the indirection isn't worth a new dep).
77
+ */
78
+ async function imageSourceOptions(): Promise<Record<string, unknown>> {
79
+ const mode = process.env['TEST_ONLINE_VM_IMAGE_SOURCE'] ?? 'oci-archive';
80
+ if (mode === 'registry') return {};
81
+ const ref =
82
+ process.env['TEST_ONLINE_VM_IMAGE'] ??
83
+ // Must match the default in main() below — otherwise the OCI
84
+ // archive's recorded ref won't match what Image.build expects.
85
+ 'essential-apps/shopify-test-vm:latest';
86
+ if (mode === 'oci-archive') {
87
+ const prep = await prepareOciArchive(ref);
88
+ if (prep.freshlySaved) {
89
+ console.error(
90
+ `[runVm] saved ${ref} → ${prep.archivePath} (${(prep.sizeBytes / 1024 / 1024).toFixed(1)} MB)`,
91
+ );
92
+ }
93
+ return { source: 'oci-archive', sourcePath: prep.archivePath };
94
+ }
95
+ if (mode === 'oci-layout') {
96
+ const sourcePath = process.env['TEST_ONLINE_VM_IMAGE_LAYOUT_PATH'];
97
+ if (!sourcePath) {
98
+ throw new Error(
99
+ `TEST_ONLINE_VM_IMAGE_SOURCE=oci-layout requires TEST_ONLINE_VM_IMAGE_LAYOUT_PATH`,
100
+ );
101
+ }
102
+ return { source: 'oci-layout', sourcePath };
103
+ }
104
+ throw new Error(
105
+ `TEST_ONLINE_VM_IMAGE_SOURCE=${mode} not recognised. Valid: oci-archive | oci-layout | registry`,
106
+ );
107
+ }
108
+
109
+ async function main(): Promise<void> {
110
+ const appName = readAppName();
111
+ // node_modules now lives in a virtio-blk VOLUME (sparse host file)
112
+ // instead of a virtio-fs bind mount. Three wins:
113
+ // 1. ~native disk speed for the many-small-files workload that
114
+ // npm install actually is (10-30s vs 3-6 min on a virtio-fs
115
+ // mount per the VM runtime's example 07).
116
+ // 2. State semantics make sense: node_modules is VM-private
117
+ // (produced by in-VM npm install), not shared with the host.
118
+ // 3. Cache invalidation via `extraFiles` on the tarball manifest
119
+ // (see below) handles "tarballs changed → rebake" properly,
120
+ // so pack-into no longer needs the wipe-host-cache hack.
121
+ // Path defaults stay namespaced by appName so multi-app dev works.
122
+ const linuxModulesVolume =
123
+ process.env['TEST_LINUX_NODE_MODULES_VOLUME'] ??
124
+ resolve(homedir(), `.cache/${appName}-test/node_modules.img`);
125
+ mkdirSync(resolve(linuxModulesVolume, '..'), { recursive: true });
126
+
127
+ // Tarball-manifest file written by `scripts/pack-into.sh`. We pass
128
+ // it as an extraFile so its bytes are folded into the snapshot's
129
+ // input hash — any tarball change re-bakes automatically. Optional
130
+ // (consumer might not use pack-into); we just skip the extraFile if
131
+ // it's missing.
132
+ const tarballManifest = resolve(
133
+ repoRoot,
134
+ 'vendor/essential-apps-shopify-test/.tarball-manifest',
135
+ );
136
+ const tarballManifestExists = existsSync(tarballManifest);
137
+
138
+ // Fold the manifest hash into the warmupTag. The `extraFiles` entry
139
+ // below does NOT reliably re-key the snapshot in this VM-runtime
140
+ // version (verified on the offline runner: manifest changed, bake
141
+ // was still 0.0s warm). The warmupTag DOES definitively key it, so
142
+ // hash the manifest (a sha256 list of the vendored tarballs) into
143
+ // the tag: tarball change → manifest change → tag change → rebake →
144
+ // warmup reinstalls, with no manual bump. `v15` is the warmup-SCRIPT
145
+ // version — bump only for warmup-step changes (v15: a failed
146
+ // npm install now aborts the bake instead of caching a half-
147
+ // installed node_modules volume).
148
+ const manifestHash = tarballManifestExists
149
+ ? createHash('sha256').update(readFileSync(tarballManifest)).digest('hex').slice(0, 12)
150
+ : 'no-manifest';
151
+
152
+ // Fold the @supermachine/core (VM runtime) version into the snapshot
153
+ // key too: the bake hash keys on GUEST inputs (image + tarballs), not
154
+ // the host VMM, so a runtime bump would otherwise warm-restore a stale
155
+ // snapshot baked by the old runtime. Key on the installed version so a
156
+ // bump auto-rebakes — same philosophy as the manifest hash. Resolved
157
+ // from the consumer root; degrades to a stable bucket if unreadable.
158
+ const runtimeVersion = ((): string => {
159
+ try {
160
+ const requirePkg = createRequire(resolve(repoRoot, 'package.json'));
161
+ return (
162
+ requirePkg('@supermachine/core/package.json') as { version: string }
163
+ ).version;
164
+ } catch {
165
+ return 'unknown';
166
+ }
167
+ })();
168
+ const runtimeTag = `sm${runtimeVersion.replace(/[^0-9a-z]+/gi, '')}`;
169
+ // Scope the snapshot to the consuming app — see runOffline.ts. Warm
170
+ // snapshots are keyed (image + warmupTag) and the image is shared, so
171
+ // two apps vendoring the same shopify-test build would otherwise
172
+ // collide on one warm snapshot (which captures the app's npm install).
173
+ const warmupTag = `online-v15-${appName}-${runtimeTag}-${manifestHash}`;
174
+
175
+ // Dynamic import — @supermachine/core is heavy and only needed
176
+ // when actually running tests, not for `--help` etc.
177
+ const { Image } = await import('@supermachine/core');
178
+
179
+ const imageOpts = await imageSourceOptions();
180
+ console.error(`[runVm] baking image… (warmupTag=${warmupTag})`);
181
+ const tBake = performance.now();
182
+ // arm64 native — no Rosetta translation tax. Storepool +
183
+ // captureAuth use patchright's bundled chromium (arm64 build
184
+ // ships with the package), so we no longer need amd64+Rosetta
185
+ // for real Google Chrome.
186
+ const platform = process.env['TEST_ONLINE_VM_PLATFORM'] ?? 'linux/arm64';
187
+
188
+ // `cmd` overrides only the image's CMD; VM runtime 0.7.16+
189
+ // auto-prepends the image's ENTRYPOINT (Docker-like semantics).
190
+ // The amd64 image's entrypoint.sh starts Xvfb + Postgres +
191
+ // /etc/hosts seed before exec-ing the cmd args; those services
192
+ // are preserved across snapshot/restore as background processes.
193
+ const image = await Image.build({
194
+ ref:
195
+ process.env['TEST_ONLINE_VM_IMAGE'] ??
196
+ // arm64-native conformance image (Dockerfile.vm).
197
+ // We switched the browser to patchright-only (storePool +
198
+ // captureAuth both use the bundled chromium with stealth
199
+ // patches), so we no longer need the amd64 image to host
200
+ // google-chrome-stable amd64 — arm64 native is faster
201
+ // (no Rosetta translation) and uses less memory.
202
+ 'essential-apps/shopify-test-vm:latest',
203
+ // 12 GiB ceiling — patchright chromium + postgres + Vite/Remix
204
+ // (Prisma + esbuild + 2k+ packages loaded) + npm-install warmup
205
+ // all live concurrently. 8 GiB OOM-killed Vite during Playwright
206
+ // startup. Lazy CoW means host phys_footprint is far smaller;
207
+ // bump via TEST_ONLINE_VM_MEMORY_MIB for memory-hungry suites.
208
+ memoryMib: Number(process.env['TEST_ONLINE_VM_MEMORY_MIB'] ?? 12288),
209
+ vcpus: Number(process.env['TEST_ONLINE_VM_VCPUS'] ?? 4),
210
+ cmd: ['sleep', 'infinity'],
211
+ // SKIP_OFFLINE_HOST_HIJACK=1 — image's entrypoint.sh would
212
+ // otherwise redirect *.shopify.com → 127.0.0.1 for offline-mode
213
+ // wiring. Online hits REAL Shopify endpoints; need real DNS.
214
+ env: { SKIP_OFFLINE_HOST_HIJACK: '1' },
215
+ ...(platform ? { platform } : {}),
216
+ mounts: [
217
+ // Workspace stays a virtio-fs MOUNT — host edits live in
218
+ // the guest. node_modules moved to a volume below.
219
+ // `guestPath` is required in the VM runtime 0.7.28+; init-oci
220
+ // auto-mounts at this path in declared order BEFORE warmup
221
+ // fires, so we no longer mount manually in the warmup script.
222
+ { hostPath: repoRoot, guestTag: 'workspace', guestPath: '/workspace' },
223
+ ],
224
+ volumes: [
225
+ // node_modules: virtio-blk volume mounted at /workspace/node_modules.
226
+ // The first volume → /dev/vdb (root is /dev/vda). Warmup formats
227
+ // it on first bake; subsequent restores re-mount it as-is.
228
+ // 4 GiB cap (sparse, only uses what's actually written).
229
+ {
230
+ hostPath: linuxModulesVolume,
231
+ guestPath: '/workspace/node_modules',
232
+ sizeMib: 4096,
233
+ },
234
+ ],
235
+ extraFiles: tarballManifestExists
236
+ ? [
237
+ // Folded into bake input-hash; any tarball change → manifest
238
+ // bytes change → new snapshot. Guest path is irrelevant
239
+ // beyond not colliding with anything else.
240
+ { hostPath: tarballManifest, guestPath: '/etc/.shopify-test-tarball-manifest' },
241
+ ]
242
+ : [],
243
+ // Snapshot key — computed from the tarball-manifest hash (see
244
+ // above) so a tarball refresh auto-rebakes without a manual bump.
245
+ // Warmup runs ONCE per bake; warm acquires skip it.
246
+ warmupTag,
247
+ // @ts-expect-error — @supermachine/core's BuildOptions type
248
+ // doesn't declare `warmup`, but the runtime accepts it. Tracked
249
+ // upstream; fix-or-augment when the types catch up.
250
+ warmup: async (vm: {
251
+ exec: (opts: { argv: string[]; timeoutMs?: number }) => Promise<{
252
+ exitCode: number;
253
+ stdout: Buffer;
254
+ stderr: Buffer;
255
+ }>;
256
+ }) => {
257
+ const r = await vm.exec({
258
+ argv: ['sh', '-c', `
259
+ set -e
260
+
261
+ # VM runtime 0.7.28+ auto-mounts both /workspace
262
+ # (virtio-fs) and /workspace/node_modules (virtio-blk ext4
263
+ # volume) BEFORE warmup fires — no manual mount/format
264
+ # needed here. We keep one defence-in-depth check though:
265
+ # assert /workspace/node_modules is NOT virtiofs. If the
266
+ # volume auto-mount silently failed, /workspace/node_modules
267
+ # would surface the HOST's repo node_modules via the
268
+ # workspace bind, and the rm-rf below would corrupt the
269
+ # @supermachine binary and many other things mid-bake.
270
+ NM_FS=\$(stat -f -c %T /workspace/node_modules 2>/dev/null || stat --file-system --format=%T /workspace/node_modules)
271
+ echo "[volume] /workspace/node_modules filesystem type: \$NM_FS"
272
+ if [ "\$NM_FS" = "virtiofs" ] || [ "\$NM_FS" = "fuse.virtiofs" ]; then
273
+ echo "FATAL: /workspace/node_modules is \$NM_FS — auto-mount broken"
274
+ echo "/proc/mounts:"; grep ' /workspace' /proc/mounts || echo " (no /workspace entries)"
275
+ exit 1
276
+ fi
277
+
278
+ echo "--- repair perms for postgres ---"
279
+ chown -R postgres:postgres /var/lib/postgresql/data
280
+ chmod 700 /var/lib/postgresql/data
281
+ mkdir -p /var/run/postgresql
282
+ chown postgres:postgres /var/run/postgresql
283
+ chmod 775 /var/run/postgresql
284
+
285
+ echo "--- npm install (fresh per rebake) ---"
286
+ # Warmup runs exactly when the snapshot is rebaked — which
287
+ # happens only when the tarball-manifest extraFile changes
288
+ # (= someone ran pack-into.sh) or the warmupTag bumps.
289
+ # Either way we want a fresh install: wipe the volume's
290
+ # node_modules dir first so npm can't reuse stale tarballs.
291
+ cd /workspace
292
+ rm -rf node_modules/* node_modules/.[!.]* 2>/dev/null || true
293
+ # npm gates optionalDependencies by os/cpu automatically,
294
+ # so darwin-arm64-only natives (e.g. @supermachine/core-darwin-arm64)
295
+ # are skipped on Linux without needing --no-optional. Any
296
+ # "notsup os/cpu" failure here means a darwin-only package
297
+ # has snuck into hard "dependencies" — fix it in package.json
298
+ # rather than papering over with --no-optional, since that
299
+ # would also skip genuinely-needed Linux optionals.
300
+ # NB: do not pipe npm to "tail" — the pipeline exit status is
301
+ # tail's (0), which masks npm failures under "set -e", so a
302
+ # transient install error (e.g. ECONNRESET fetching prisma
303
+ # engines) would bake a snapshot with a half-installed
304
+ # node_modules volume that every warm restore then reuses.
305
+ # Capture to a file and check npm's own status: a failed
306
+ # install aborts the bake (no snapshot cached, next run
307
+ # rebakes), so a flaky-network blip self-heals.
308
+ if ! npm install --legacy-peer-deps --engine-strict=false > /tmp/npm-install.log 2>&1; then
309
+ echo "--- npm install FAILED - last 40 lines ---"
310
+ tail -40 /tmp/npm-install.log
311
+ exit 1
312
+ fi
313
+ tail -30 /tmp/npm-install.log
314
+
315
+ echo "--- patch /etc/hosts for localhost ---"
316
+ if ! grep -q "^127.0.0.1.*localhost" /etc/hosts; then
317
+ echo "127.0.0.1 localhost" >> /etc/hosts
318
+ echo "::1 localhost" >> /etc/hosts
319
+ fi
320
+
321
+ echo "--- warmup done ---"
322
+ `],
323
+ timeoutMs: 15 * 60 * 1000,
324
+ });
325
+ // Always print warmup output so silent failures (npm install
326
+ // writing to wrong dir, mounts attaching wrong tag, etc.) are
327
+ // visible.
328
+ console.error(`[runVm.warmup] stdout (${r.stdout.length} bytes):\n${r.stdout.toString()}`);
329
+ if (r.stderr.length > 0) {
330
+ console.error(`[runVm.warmup] stderr (${r.stderr.length} bytes):\n${r.stderr.toString()}`);
331
+ }
332
+ if (r.exitCode !== 0) {
333
+ throw new Error(`warmup failed (exit ${r.exitCode}) — see output above`);
334
+ }
335
+ },
336
+ ...imageOpts,
337
+ });
338
+ console.error(`[runVm] bake: ${((performance.now() - tBake) / 1000).toFixed(1)} s`);
339
+
340
+ const pool = await image.pool({ min: 1, max: 1, restoreOnRelease: false });
341
+ const vm = await pool.acquire();
342
+ try {
343
+ // Post-restore sanity. VM runtime 0.7.28+ captures the
344
+ // auto-mount tree in the snapshot, so /workspace and
345
+ // /workspace/node_modules are already mounted on restore. We
346
+ // just assert + bail if anything's off; same defence-in-depth
347
+ // virtiofs check as the warmup to prevent rm-by-accident if
348
+ // auto-mount layering ever breaks.
349
+ const tMount = performance.now();
350
+ const mountR = await vm.exec({
351
+ argv: ['sh', '-c', `
352
+ mountpoint -q /workspace || { echo "FATAL: /workspace not mounted"; exit 1; }
353
+ mountpoint -q /workspace/node_modules || { echo "FATAL: /workspace/node_modules not mounted"; exit 1; }
354
+ NM_FS=\$(stat -f -c %T /workspace/node_modules 2>/dev/null)
355
+ if [ "\$NM_FS" = "virtiofs" ] || [ "\$NM_FS" = "fuse.virtiofs" ]; then
356
+ echo "FATAL: /workspace/node_modules is \$NM_FS — auto-mount layering broken"
357
+ exit 1
358
+ fi
359
+ ls /workspace/node_modules/@essential-apps/shopify-test-runner/dist/scripts/runTests.js >/dev/null \
360
+ || { echo "FATAL: runTests.js missing in volume — snapshot/volume out of sync"; ls /workspace/node_modules/@essential-apps 2>&1 | head; exit 1; }
361
+ `],
362
+ timeoutMs: 10_000,
363
+ });
364
+ if (mountR.exitCode !== 0) {
365
+ console.error(`[runVm] mount failed:\n${mountR.stdout.toString()}${mountR.stderr.toString()}`);
366
+ process.exit(1);
367
+ }
368
+ console.error(`[runVm] mounts: ${((performance.now() - tMount) / 1000).toFixed(2)} s`);
369
+
370
+ // Postgres startup — fast because warmup already chowned the data dir.
371
+ const tPg = performance.now();
372
+ const pgR = await vm.exec({
373
+ argv: ['sh', '-c', `
374
+ export PGDATA=/var/lib/postgresql/data
375
+ export PG_BIN=/usr/lib/postgresql/14/bin
376
+ # Use pg_isready (actually checks ACCEPT-CONNECTIONS) rather
377
+ # than pg_ctl status (which just checks postmaster.pid exists).
378
+ # The snapshot can capture a stale postmaster.pid file from
379
+ # the bake-time postgres that didn't survive restore, so
380
+ # pg_ctl status returns 0 but pg_isready / actual connections
381
+ # both fail. Always prefer the real readiness probe.
382
+ if su postgres -c "\${PG_BIN}/pg_isready -h 127.0.0.1 -p 5432 -q" >/dev/null 2>&1; then exit 0; fi
383
+ # Postgres isn't actually serving — clean up any stale state
384
+ # from the snapshot and start fresh.
385
+ rm -f \${PGDATA}/postmaster.pid 2>/dev/null || true
386
+ if [ -z "$(ls -A \${PGDATA} 2>/dev/null)" ]; then su postgres -c "\${PG_BIN}/initdb -D \${PGDATA} -A trust" >/dev/null; fi
387
+ su postgres -c "\${PG_BIN}/pg_ctl -D \${PGDATA} -l /tmp/pg.log -o '-h 127.0.0.1 -p 5432' start" >/dev/null
388
+ for _ in $(seq 1 50); do
389
+ if su postgres -c "\${PG_BIN}/pg_isready -h 127.0.0.1 -p 5432" >/dev/null 2>&1; then
390
+ # Create 'root' superuser to match the libkrun image entrypoint
391
+ # — runTests.js connects as 'root'.
392
+ su postgres -c "\${PG_BIN}/createuser -h 127.0.0.1 -p 5432 -s root" 2>&1 | grep -v 'already exists' || true
393
+ exit 0
394
+ fi
395
+ sleep 0.1
396
+ done
397
+ cat /tmp/pg.log
398
+ exit 1
399
+ `],
400
+ timeoutMs: 30_000,
401
+ });
402
+ if (pgR.exitCode !== 0) {
403
+ console.error('[runVm] postgres start failed:', pgR.stderr.toString());
404
+ process.exit(1);
405
+ }
406
+ console.error(`[runVm] postgres: ${((performance.now() - tPg) / 1000).toFixed(2)} s`);
407
+
408
+ // Xvfb: snapshot/restore preserves /tmp/.X11-unix/X99 (socket
409
+ // file) AND /tmp/.X99-lock (lock file). After restore, the
410
+ // entrypoint's `Xvfb :99 &` call refuses to start ("server :99
411
+ // already in use") because the stale lock is present — but the
412
+ // STALE SOCKET file is also there, so a `[ -e socket ]` check
413
+ // passes spuriously and Chrome gets "Missing X server or
414
+ // $DISPLAY" when it tries to actually connect. Same failure
415
+ // mode we hit in runVmAuth.ts; same fix: kill any
416
+ // existing Xvfb, remove the stale lock/socket, start fresh.
417
+ // Always start Xvfb — storePool launches HEADED chromium (matches
418
+ // captureAuth's fingerprint for cf_clearance carryover), and
419
+ // headed chromium needs an X server regardless of arch.
420
+ const needsXvfb = true;
421
+ if (needsXvfb) {
422
+ const tXvfb = performance.now();
423
+ const xR = await vm.exec({
424
+ argv: ['bash', '-c', `
425
+ # pkill -x (process-name exact match) — NOT pkill -f, which
426
+ # would also match the script's own argv and kill ourselves.
427
+ pkill -x Xvfb 2>/dev/null || true
428
+ rm -f /tmp/.X11-unix/X99 /tmp/.X99-lock
429
+ # -ac: no Xauthority required (other vm.exec / Playwright
430
+ # processes can connect without the cookie context from
431
+ # the start). Background via nohup + & so the daemon
432
+ # outlives this exec call.
433
+ nohup Xvfb :99 -screen 0 1600x1000x24 -nolisten tcp -ac \
434
+ >/var/log/xvfb.log 2>&1 &
435
+ for _ in $(seq 1 150); do
436
+ [ -e /tmp/.X11-unix/X99 ] && echo ready && exit 0
437
+ sleep 0.1
438
+ done
439
+ echo "Xvfb did not bind /tmp/.X11-unix/X99 within 15s"
440
+ cat /var/log/xvfb.log
441
+ exit 1
442
+ `],
443
+ timeoutMs: 30_000,
444
+ });
445
+ if (xR.exitCode !== 0) {
446
+ console.error(`[runVm] Xvfb wait failed: ${xR.stderr.toString()}`);
447
+ process.exit(1);
448
+ }
449
+ console.error(`[runVm] Xvfb: ${((performance.now() - tXvfb) / 1000).toFixed(2)} s (${xR.stdout.toString().trim()})`);
450
+ }
451
+
452
+ // Forward a curated env subset into the test process. Anything
453
+ // beyond this is intentionally not visible to the in-VM Node
454
+ // (hermetic, same policy as runTests.js itself).
455
+ const forwardEnv: Record<string, string> = {};
456
+ for (const k of [
457
+ 'TEST_VISIBLE',
458
+ 'TEST_WORKERS',
459
+ 'TEST_PRESERVE_DB',
460
+ // TEST_ONLINE_STORE_CLEANUP_URL / TEST_ONLINE_CLEANUP_SECRET intentionally not
461
+ // forwarded — runTests.ts generates the secret per-run and
462
+ // builds the URL from devAppUrl. Forwarding stale values from
463
+ // .env.test would override that.
464
+ 'TEST_OFFLINE_STRICT_GRAPHQL',
465
+ // TEST_FORCE_REAL_CHROME: storePool flips chromium.launch to
466
+ // `channel: 'chrome'` (real Google Chrome amd64 installed in
467
+ // the libkrun-style image). Set this together with
468
+ // TEST_ONLINE_VM_IMAGE=…shopify-test:latest +
469
+ // TEST_ONLINE_VM_PLATFORM=linux/amd64.
470
+ 'TEST_FORCE_REAL_CHROME',
471
+ // PLAYWRIGHT_GREP narrows the test set inside the VM. runTests.ts
472
+ // reads this and forwards as `playwright test --grep`. Without
473
+ // this passthrough, an env var the user sets on the host invocation
474
+ // would silently get dropped and the whole suite would run.
475
+ 'PLAYWRIGHT_GREP',
476
+ 'CI',
477
+ 'DEBUG',
478
+ ]) {
479
+ const v = process.env[k];
480
+ if (v !== undefined) forwardEnv[k] = v;
481
+ }
482
+ // Mark we're inside the test container — storePool reads this to
483
+ // adjust chromium launch flags.
484
+ forwardEnv['TEST_IN_CONTAINER'] = 'true';
485
+ // Wire DISPLAY for headed Chrome. Matches the Xvfb start above.
486
+ if (needsXvfb) forwardEnv['DISPLAY'] = ':99';
487
+
488
+ // Spawn the test runner. Streaming stdout/stderr via spawn so
489
+ // the user sees Playwright output as it lands rather than at end.
490
+ console.error('[runVm] launching runTests.js…');
491
+ const tTests = performance.now();
492
+ const testProc = await vm.spawn({
493
+ argv: ['sh', '-c', `
494
+ cd /workspace
495
+ # node_modules/.bin on PATH so test code can shell out to
496
+ # local CLIs (shopify, prisma, playwright). Matches the
497
+ # libkrun image's entrypoint.sh.
498
+ export PATH=/workspace/node_modules/.bin:$PATH
499
+ # Merge stderr → stdout so the stall guard sees Node errors
500
+ # (which Node writes to stderr) and surfaces them to the
501
+ # operator instead of dying silently after the stall timeout.
502
+ exec node ${envFileArgs(repoRoot)} node_modules/@essential-apps/shopify-test-runner/dist/scripts/runTests.js 2>&1
503
+ `],
504
+ env: forwardEnv,
505
+ });
506
+
507
+ // Stream + collect output. Hard 10-min cap by default, with a
508
+ // 60 s no-output stall guard (mirrors the offline-full bench's
509
+ // proven pattern for live processes).
510
+ const maxMs = Number(process.env['TEST_ONLINE_VM_TIMEOUT_MS'] ?? 10 * 60 * 1000);
511
+ // Stall guard: kill the test process if no output for N seconds.
512
+ // Default 60s; bump via TEST_ONLINE_VM_STALL_MS. amd64+Rosetta
513
+ // can take >60s for Vite/Remix initial compile so bump for that
514
+ // path explicitly.
515
+ const stallMs = Number(
516
+ process.env['TEST_ONLINE_VM_STALL_MS'] ??
517
+ (platform === 'linux/amd64' ? 300_000 : 60_000),
518
+ );
519
+ const start = Date.now();
520
+ let lastByteAt = Date.now();
521
+ while (true) {
522
+ const out = await testProc.readStdout(64 * 1024);
523
+ const now = Date.now();
524
+ if (out.length === 0) {
525
+ if (now - start > maxMs) {
526
+ console.error(`\n[runVm] HARD TIMEOUT ${maxMs / 1000}s — killing`);
527
+ await testProc.signal(15).catch(() => {});
528
+ break;
529
+ }
530
+ if (now - lastByteAt > stallMs) {
531
+ console.error(`\n[runVm] no output for ${stallMs / 1000}s — killing`);
532
+ await testProc.signal(15).catch(() => {});
533
+ break;
534
+ }
535
+ await new Promise((r) => setTimeout(r, 200));
536
+ continue;
537
+ }
538
+ lastByteAt = now;
539
+ process.stdout.write(out);
540
+ // Playwright summary lines look like " 17 passed (45.6s)" or
541
+ // " 3 failed". Once we see one, drain the tail and break.
542
+ if (/\b\d+ (passed|failed)\b/.test(out.toString())) {
543
+ await new Promise((r) => setTimeout(r, 500));
544
+ const tail = await testProc.readStdout(64 * 1024);
545
+ if (tail.length > 0) process.stdout.write(tail);
546
+ break;
547
+ }
548
+ }
549
+ const wait = await testProc.wait();
550
+ console.error(`[runVm] tests: ${((performance.now() - tTests) / 1000).toFixed(1)} s`);
551
+ console.error(`[runVm] grand: ${((performance.now() - tBake) / 1000).toFixed(1)} s`);
552
+ process.exit(wait.exitCode ?? 0);
553
+ } finally {
554
+ await vm.release().catch(() => {});
555
+ await pool.shutdown().catch(() => {});
556
+ }
557
+ }
558
+
559
+ main().catch((err) => {
560
+ console.error(err);
561
+ process.exit(1);
562
+ });