@essential-apps/shopify-test-runner 1.0.12 → 1.0.13

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 (96) hide show
  1. package/dist/lib/guestVnc.d.ts +31 -0
  2. package/dist/lib/guestVnc.d.ts.map +1 -0
  3. package/dist/lib/guestVnc.js +111 -0
  4. package/dist/lib/guestVnc.js.map +1 -0
  5. package/dist/probes/runProbe.js +0 -0
  6. package/dist/scripts/addStore.js +0 -0
  7. package/dist/scripts/buildImage.d.ts +3 -0
  8. package/dist/scripts/buildImage.d.ts.map +1 -0
  9. package/dist/scripts/{buildDockerImage.js → buildImage.js} +12 -10
  10. package/dist/scripts/buildImage.js.map +1 -0
  11. package/dist/scripts/captureAuth.js +0 -0
  12. package/dist/scripts/captureContracts.js +0 -0
  13. package/dist/scripts/captureRestContracts.js +0 -0
  14. package/dist/scripts/captureSharedContracts.d.ts +3 -0
  15. package/dist/scripts/captureSharedContracts.d.ts.map +1 -0
  16. package/dist/scripts/captureSharedContracts.js +209 -0
  17. package/dist/scripts/captureSharedContracts.js.map +1 -0
  18. package/dist/scripts/checkOperationCoverage.js +0 -0
  19. package/dist/scripts/cleanupStores.js +0 -0
  20. package/dist/scripts/createStores.js +0 -0
  21. package/dist/scripts/deployAppVersion.js +0 -0
  22. package/dist/scripts/devOnlineBackend.js +0 -0
  23. package/dist/scripts/installApp.js +0 -0
  24. package/dist/scripts/listStores.js +0 -0
  25. package/dist/scripts/runOffline.js +78 -1
  26. package/dist/scripts/runOffline.js.map +1 -1
  27. package/dist/scripts/runOfflineFullTests.js +49 -21
  28. package/dist/scripts/runOfflineFullTests.js.map +1 -1
  29. package/dist/scripts/runTests.js +0 -0
  30. package/dist/scripts/runVm.js +0 -0
  31. package/dist/scripts/runVmAuth.js +0 -0
  32. package/dist/scripts/setupTestDb.js +0 -0
  33. package/dist/scripts/verifyContracts.js +20 -29
  34. package/dist/scripts/verifyContracts.js.map +1 -1
  35. package/dist/scripts/verifyRestContracts.js +17 -30
  36. package/dist/scripts/verifyRestContracts.js.map +1 -1
  37. package/package.json +11 -9
  38. package/src/lib/guestVnc.ts +147 -0
  39. package/src/scripts/{buildDockerImage.ts → buildImage.ts} +11 -9
  40. package/src/scripts/captureSharedContracts.ts +228 -0
  41. package/src/scripts/runOffline.ts +82 -1
  42. package/src/scripts/runOfflineFullTests.ts +56 -21
  43. package/src/scripts/verifyContracts.ts +22 -38
  44. package/src/scripts/verifyRestContracts.ts +23 -42
  45. package/dist/edge/nodeShim.d.ts +0 -2
  46. package/dist/edge/nodeShim.d.ts.map +0 -1
  47. package/dist/edge/nodeShim.js +0 -217
  48. package/dist/edge/nodeShim.js.map +0 -1
  49. package/dist/scripts/_probeSourceUrl.d.ts +0 -3
  50. package/dist/scripts/_probeSourceUrl.d.ts.map +0 -1
  51. package/dist/scripts/_probeSourceUrl.js +0 -119
  52. package/dist/scripts/_probeSourceUrl.js.map +0 -1
  53. package/dist/scripts/buildDockerImage.d.ts +0 -3
  54. package/dist/scripts/buildDockerImage.d.ts.map +0 -1
  55. package/dist/scripts/buildDockerImage.js.map +0 -1
  56. package/dist/scripts/devE2eBackend.d.ts +0 -3
  57. package/dist/scripts/devE2eBackend.d.ts.map +0 -1
  58. package/dist/scripts/devE2eBackend.js +0 -117
  59. package/dist/scripts/devE2eBackend.js.map +0 -1
  60. package/dist/scripts/runDocker.d.ts +0 -3
  61. package/dist/scripts/runDocker.d.ts.map +0 -1
  62. package/dist/scripts/runDocker.js +0 -88
  63. package/dist/scripts/runDocker.js.map +0 -1
  64. package/dist/scripts/runDockerAuth.d.ts +0 -3
  65. package/dist/scripts/runDockerAuth.d.ts.map +0 -1
  66. package/dist/scripts/runDockerAuth.js +0 -108
  67. package/dist/scripts/runDockerAuth.js.map +0 -1
  68. package/dist/scripts/runDockerOffline.d.ts +0 -3
  69. package/dist/scripts/runDockerOffline.d.ts.map +0 -1
  70. package/dist/scripts/runDockerOffline.js +0 -129
  71. package/dist/scripts/runDockerOffline.js.map +0 -1
  72. package/dist/scripts/runDockerOfflineExplore.d.ts +0 -3
  73. package/dist/scripts/runDockerOfflineExplore.d.ts.map +0 -1
  74. package/dist/scripts/runDockerOfflineExplore.js +0 -116
  75. package/dist/scripts/runDockerOfflineExplore.js.map +0 -1
  76. package/dist/scripts/runIsolatedDockerOffline.d.ts +0 -3
  77. package/dist/scripts/runIsolatedDockerOffline.d.ts.map +0 -1
  78. package/dist/scripts/runIsolatedDockerOffline.js +0 -351
  79. package/dist/scripts/runIsolatedDockerOffline.js.map +0 -1
  80. package/dist/scripts/runOfflineE2e.d.ts +0 -3
  81. package/dist/scripts/runOfflineE2e.d.ts.map +0 -1
  82. package/dist/scripts/runOfflineE2e.js +0 -408
  83. package/dist/scripts/runOfflineE2e.js.map +0 -1
  84. package/dist/scripts/runSupermachine.d.ts +0 -3
  85. package/dist/scripts/runSupermachine.d.ts.map +0 -1
  86. package/dist/scripts/runSupermachine.js +0 -474
  87. package/dist/scripts/runSupermachine.js.map +0 -1
  88. package/dist/scripts/runSupermachineAuth.d.ts +0 -3
  89. package/dist/scripts/runSupermachineAuth.d.ts.map +0 -1
  90. package/dist/scripts/runSupermachineAuth.js +0 -454
  91. package/dist/scripts/runSupermachineAuth.js.map +0 -1
  92. package/dist/vite/offlineConfig.d.ts +0 -34
  93. package/dist/vite/offlineConfig.d.ts.map +0 -1
  94. package/dist/vite/offlineConfig.js +0 -61
  95. package/dist/vite/offlineConfig.js.map +0 -1
  96. package/src/scripts/runDockerAuth.ts +0 -120
@@ -56,6 +56,7 @@ import { dirname, resolve } from 'node:path';
56
56
  import { fileURLToPath } from 'node:url';
57
57
  import { pickApp, prepareOciArchive, envFileArgs, computeBuildHash } from '@essential-apps/shopify-test-core';
58
58
  import { buildExtensions } from '../lib/buildExtensions.js';
59
+ import { exposeGuestVnc } from '../lib/guestVnc.js';
59
60
 
60
61
  const repoRoot = process.cwd();
61
62
 
@@ -338,6 +339,27 @@ async function main(): Promise<void> {
338
339
  }
339
340
  })();
340
341
  const runtimeTag = `sm${runtimeVersion.replace(/[^0-9a-z]+/gi, '')}`;
342
+
343
+ // Fold THIS runner package's own version into the snapshot key. The
344
+ // warm snapshot bakes the in-VM `npm install` (which installs the
345
+ // orchestrator + fixtures this same shopify-test version ships). Those
346
+ // run INSIDE the VM, so a version bump that changes orchestrator/
347
+ // fixture behaviour (e.g. screen-video support) would otherwise
348
+ // warm-restore a STALE in-VM install — the consumer updates the
349
+ // package but keeps running the old in-VM code until they manually
350
+ // `rm -rf ~/.cache/<app>-test`. Keying on the runner version makes a
351
+ // bump auto-rebake (cold install of the new version) — same
352
+ // no-manual-bust philosophy as manifestHash + runtimeTag.
353
+ const runnerVersion = ((): string => {
354
+ try {
355
+ return (
356
+ createRequire(import.meta.url)('../../package.json') as { version: string }
357
+ ).version;
358
+ } catch {
359
+ return 'unknown';
360
+ }
361
+ })();
362
+ const runnerTag = `r${runnerVersion.replace(/[^0-9a-z]+/gi, '')}`;
341
363
  // Scope the snapshot to the consuming app. supermachine keys WARM
342
364
  // snapshots by (image + warmupTag), but the image is shared across
343
365
  // apps and the rest of the tag is identical for any two apps vendoring
@@ -347,7 +369,7 @@ async function main(): Promise<void> {
347
369
  // identity (observed: essential-upsell booting with essential-seo's
348
370
  // DB). The cache paths are already appName-namespaced; this aligns the
349
371
  // snapshot key with them.
350
- const warmupTag = `offline-v13-${appName}-${runtimeTag}-${manifestHash}`;
372
+ const warmupTag = `offline-v13-${appName}-${runtimeTag}-${runnerTag}-${manifestHash}`;
351
373
 
352
374
  // Dynamic import — @supermachine/core is heavy and only needed
353
375
  // when actually running tests, not for `--help` etc.
@@ -513,6 +535,15 @@ async function main(): Promise<void> {
513
535
  // orchestrator's Xvfb startup + the offlineFullStack screen-video
514
536
  // fixture).
515
537
  'TEST_OFFLINE_VIDEO',
538
+ // Offline runs headed-under-Xvfb by default; opt out to headless.
539
+ 'TEST_OFFLINE_HEADLESS',
540
+ // Interactive explore mode: orchestrator skips Playwright and
541
+ // instead opens a headed Chromium (admin + storefront tabs) on
542
+ // the guest Xvfb display, which we surface over VNC (see the
543
+ // explore branch below).
544
+ 'TEST_OFFLINE_EXPLORE',
545
+ 'TEST_OFFLINE_EXPLORE_URL',
546
+ 'TEST_OFFLINE_EXPLORE_NO_DEVTOOLS',
516
547
  'CI',
517
548
  'DEBUG',
518
549
  ]) {
@@ -585,6 +616,56 @@ async function main(): Promise<void> {
585
616
  forwardEnv['PRISMA_QUERY_ENGINE_LIBRARY'] =
586
617
  '/workspace/node_modules/.prisma/client/libquery_engine-linux-arm64-openssl-3.0.x.so.node';
587
618
 
619
+ // ── Explore mode (interactive, VNC) ───────────────────────────
620
+ // `TEST_OFFLINE_EXPLORE=true` boots the full offline mock stack
621
+ // and hands the developer a real browser inside the VM instead of
622
+ // running Playwright. We surface the guest's Xvfb display over VNC
623
+ // (supermachine `vm.exposeTcp`, NOT docker --publish) so they can
624
+ // click through the admin app + storefront against the mock stack.
625
+ // The orchestrator opens both tabs (see runOfflineFullTests explore
626
+ // branch) and holds until the browser window is closed / Ctrl-C.
627
+ if (process.env['TEST_OFFLINE_EXPLORE'] === 'true') {
628
+ let forwarder: { stop?: () => Promise<void> | void } | undefined;
629
+ try {
630
+ forwarder = (await exposeGuestVnc(vm, {
631
+ logPrefix: '[runOffline]',
632
+ })) as { stop?: () => Promise<void> | void };
633
+
634
+ console.error('[runOffline] launching explore session…');
635
+ const exploreProc = await vm.spawn({
636
+ argv: ['sh', '-c', `
637
+ cd /workspace
638
+ export PATH=/workspace/node_modules/.bin:$PATH
639
+ export DISPLAY=:99
640
+ node ${envFileArgs(repoRoot)} node_modules/@essential-apps/shopify-test-runner/dist/scripts/runOfflineFullTests.js
641
+ `],
642
+ env: forwardEnv,
643
+ });
644
+
645
+ // Stream guest output verbatim with NO stall/hard timeout — an
646
+ // explore session legitimately sits idle for as long as the
647
+ // developer is poking around. It ends when they close the
648
+ // browser (orchestrator exits) or Ctrl-C here.
649
+ const pump = (async () => {
650
+ while (true) {
651
+ const out = await exploreProc.readStdout(64 * 1024);
652
+ if (out.length === 0) {
653
+ await new Promise((r) => setTimeout(r, 200));
654
+ continue;
655
+ }
656
+ process.stdout.write(out);
657
+ }
658
+ })();
659
+ pump.catch(() => {});
660
+
661
+ const wait = await exploreProc.wait();
662
+ process.exitCode = wait.exitCode ?? 0;
663
+ return;
664
+ } finally {
665
+ if (forwarder?.stop) await Promise.resolve(forwarder.stop()).catch(() => {});
666
+ }
667
+ }
668
+
588
669
  // Optional --grep forwarding for fast iteration on a single
589
670
  // scenario or spec without writing a one-off bench script.
590
671
  const grep = process.env['PLAYWRIGHT_GREP'];
@@ -1229,13 +1229,15 @@ async function main(): Promise<void> {
1229
1229
 
1230
1230
  // ── Explore mode (no Playwright; manual click-around) ──
1231
1231
  // When `TEST_OFFLINE_EXPLORE=true` is set, skip Playwright
1232
- // entirely. Instead, launch a single non-headless Chrome on the
1233
- // container's Xvfb display, pointed at the admin app's entry
1234
- // URL. The container is started with `--publish 5900:5900` +
1235
- // `TEST_ONLINE_VNC=1`, so the developer can connect from macOS via
1232
+ // entirely. Instead, launch a non-headless Chrome on the guest's
1233
+ // Xvfb :99 display with TWO tabs — the admin app and the storefront
1234
+ // both pointed at the in-VM mock stack. The host-side runner
1235
+ // (`runOffline.ts`) surfaces :99 over VNC via supermachine's
1236
+ // `vm.exposeTcp` (NOT docker --publish — we don't use docker for
1237
+ // anything), so the developer connects from macOS via
1236
1238
  // open vnc://localhost:5900 (password: `test`)
1237
- // and click through the admin and storefront against the online
1238
- // mock stack. The process holds open until SIGINT/SIGTERM.
1239
+ // and clicks through admin + storefront against the mock stack.
1240
+ // The process holds open until the browser is closed / SIGINT.
1239
1241
  //
1240
1242
  // This is the "interactive Shopify dev store" mode — useful
1241
1243
  // for visually inspecting funnel rendering, debugging admin
@@ -1342,6 +1344,26 @@ async function main(): Promise<void> {
1342
1344
  exploreCtx.pages()[0] ?? (await exploreCtx.newPage());
1343
1345
  await explorePage.goto(adminUrl, { waitUntil: 'domcontentloaded' });
1344
1346
 
1347
+ // Second tab: the storefront (app embed + widget render against
1348
+ // the seeded catalog). Opening both up-front means the developer
1349
+ // lands on a ready admin + storefront without typing URLs. Errors
1350
+ // are swallowed — a slow/failed storefront load shouldn't abort
1351
+ // the whole explore session (the admin tab is still useful).
1352
+ const storefrontUrl =
1353
+ process.env['TEST_OFFLINE_EXPLORE_STOREFRONT_URL'] ??
1354
+ `https://${DEFAULT_SHOP_DOMAIN}/`;
1355
+ try {
1356
+ const sfPage = await exploreCtx.newPage();
1357
+ await sfPage.goto(storefrontUrl, { waitUntil: 'domcontentloaded' });
1358
+ } catch (err) {
1359
+ console.error(
1360
+ `[runOfflineFull] explore: storefront tab failed to load (${(err as Error).message}) — continuing`,
1361
+ );
1362
+ }
1363
+ // Leave the admin tab focused as the active one.
1364
+ await explorePage.bringToFront().catch(() => {});
1365
+ console.log(` Storefront tab opened: ${storefrontUrl}`);
1366
+
1345
1367
  // Wait indefinitely. Exit triggers:
1346
1368
  // - Developer closes the browser window → context emits `close`.
1347
1369
  // - Container receives SIGINT/SIGTERM → we close the context
@@ -1361,16 +1383,20 @@ async function main(): Promise<void> {
1361
1383
 
1362
1384
  // ── Playwright ─────────────────────────────────────────
1363
1385
  console.log('[runOfflineFull] running Playwright…');
1364
- // Opt-in screen recording: run headed under Xvfb :99 so ffmpeg can
1365
- // x11grab the display (admin + storefront, one real video). Xvfb
1366
- // ships in the Playwright base image. Start it if the entrypoint
1367
- // didn't already (idempotent reuse an existing :99 socket).
1386
+ // Headed-under-Xvfb is the DEFAULT for offline runs (production-
1387
+ // faithful, ~4% slower than headless). Opt OUT with
1388
+ // TEST_OFFLINE_HEADLESS=true. Full-screen recording (TEST_OFFLINE_VIDEO)
1389
+ // and TEST_VISIBLE always force headed. Xvfb ships in the Playwright
1390
+ // base image; ffmpeg (for recording) is baked in too. We start Xvfb
1391
+ // :99 whenever headed, and only require ffmpeg when recording.
1368
1392
  let videoOn = process.env['TEST_OFFLINE_VIDEO'] === 'true';
1393
+ const headlessOn = process.env['TEST_OFFLINE_HEADLESS'] === 'true';
1394
+ let headedOn = videoOn || process.env['TEST_VISIBLE'] === 'true' || !headlessOn;
1395
+ let headedReady = false;
1369
1396
  if (videoOn) {
1370
- const { existsSync } = await import('node:fs');
1371
- // ffmpeg (with x11grab) is baked into the VM image. An image built
1372
- // before video support won't have it degrade gracefully with a
1373
- // clear rebuild hint rather than failing every test's recording.
1397
+ // Recording needs ffmpeg (with x11grab). An image built before
1398
+ // video support won't have it degrade to headed-no-record with
1399
+ // a clear rebuild hint rather than failing every test's recording.
1374
1400
  let hasFfmpeg = false;
1375
1401
  try {
1376
1402
  const { execSync: execSyncCheck } = await import('node:child_process');
@@ -1384,19 +1410,19 @@ async function main(): Promise<void> {
1384
1410
  '[runOfflineFull] TEST_OFFLINE_VIDEO set but `ffmpeg` is missing from the ' +
1385
1411
  'VM image — rebuild it (it predates video support): ' +
1386
1412
  '`container image rm essential-apps/shopify-test-vm:latest` then re-run. ' +
1387
- 'Continuing WITHOUT video.',
1413
+ 'Continuing headed WITHOUT recording.',
1388
1414
  );
1389
1415
  videoOn = false;
1390
1416
  process.env['TEST_OFFLINE_VIDEO'] = 'false';
1391
1417
  }
1392
1418
  }
1393
- if (videoOn) {
1419
+ if (headedOn) {
1394
1420
  const { existsSync } = await import('node:fs');
1395
1421
  if (!existsSync('/tmp/.X11-unix/X99')) {
1396
1422
  const { spawn: spawnXvfb } = await import('node:child_process');
1397
1423
  const xv = spawnXvfb(
1398
1424
  'Xvfb',
1399
- [':99', '-screen', '0', '1280x720x24', '-nolisten', 'tcp', '-ac'],
1425
+ [':99', '-screen', '0', '1400x900x24', '-nolisten', 'tcp', '-ac'],
1400
1426
  { detached: true, stdio: 'ignore' },
1401
1427
  );
1402
1428
  xv.unref();
@@ -1405,17 +1431,26 @@ async function main(): Promise<void> {
1405
1431
  await new Promise((r) => setTimeout(r, 100));
1406
1432
  }
1407
1433
  }
1408
- if (!existsSync('/tmp/.X11-unix/X99')) {
1409
- console.error('[runOfflineFull] WARNING: Xvfb :99 did not start — video disabled');
1434
+ headedReady = existsSync('/tmp/.X11-unix/X99');
1435
+ if (!headedReady) {
1436
+ console.error(
1437
+ '[runOfflineFull] WARNING: Xvfb :99 did not start — falling back to headless',
1438
+ );
1439
+ videoOn = false;
1440
+ headedOn = false;
1441
+ process.env['TEST_OFFLINE_VIDEO'] = 'false';
1442
+ process.env['TEST_OFFLINE_HEADED'] = 'false';
1410
1443
  } else {
1411
- console.error('[runOfflineFull] screen video ON — Xvfb :99 ready (headed)');
1444
+ console.error(
1445
+ `[runOfflineFull] headed under Xvfb :99 (${videoOn ? 'recording' : 'no recording'})`,
1446
+ );
1412
1447
  }
1413
1448
  }
1414
1449
 
1415
1450
  const playwrightEnv: NodeJS.ProcessEnv = {
1416
1451
  ...process.env,
1417
1452
  // Point headed Chromium + the screen-video fixture at the Xvfb display.
1418
- ...(videoOn ? { DISPLAY: ':99' } : {}),
1453
+ ...(headedReady ? { DISPLAY: ':99' } : {}),
1419
1454
  // Expose the per-run test DB to specs so they can seed/inspect the
1420
1455
  // app's OWN Postgres directly (e.g. seed Article rows for
1421
1456
  // list/detail flows). The mock ShopState covers Shopify resources,
@@ -29,8 +29,8 @@
29
29
  *
30
30
  * Run: `npm run test:online:verify-contracts`.
31
31
  */
32
- import { readdirSync, readFileSync, statSync } from 'node:fs';
33
- import { resolve, relative } from 'node:path';
32
+ import { resolve } from 'node:path';
33
+ import { resolveContracts } from '@essential-apps/shopify-test-contracts/operation-contract';
34
34
  import {
35
35
  createAdminApi,
36
36
  createStorefrontApi,
@@ -112,16 +112,6 @@ function buildOfflineExecutor(api: ApiType, state: ShopState): Executor {
112
112
  };
113
113
  }
114
114
 
115
- interface Contract {
116
- operationName: string;
117
- source: string;
118
- variables: Record<string, unknown>;
119
- response: unknown;
120
- capturedFrom: 'offline' | 'live';
121
- capturedAt: string;
122
- warning?: string;
123
- }
124
-
125
115
  /**
126
116
  * Same deterministic ShopState seed `captureContracts` uses. The
127
117
  * contracts were captured against this state; verification must
@@ -214,21 +204,24 @@ function diff(
214
204
  async function main(): Promise<void> {
215
205
  const args = parseArgs();
216
206
 
217
- let entries: string[];
218
- try {
219
- entries = readdirSync(args.contractsDir);
220
- } catch {
207
+ // Merged view: shared package goldens (the centralized generic ops)
208
+ // overlaid by the app's local contracts dir, which overrides by
209
+ // operation name. An app vendors a local file only when its shape
210
+ // genuinely diverges from the shared golden.
211
+ const resolved = resolveContracts({ api: args.api, appDir: args.contractsDir });
212
+ if (resolved.length === 0) {
221
213
  console.error(
222
- `[verify-contracts] no contracts directory at ${args.contractsDir}. ` +
223
- `Run \`npm run test:online:capture-contracts\` first.`,
214
+ `[verify-contracts] no contracts for api=${args.api} ` +
215
+ `(shared package + ${args.contractsDir}).`,
224
216
  );
225
217
  process.exit(2);
226
218
  }
227
- const contractFiles = entries.filter((f) => f.endsWith('.json'));
228
- if (contractFiles.length === 0) {
229
- console.error(`[verify-contracts] no contracts found in ${args.contractsDir}`);
230
- process.exit(2);
231
- }
219
+ const sharedCount = resolved.filter((r) => r.origin === 'shared').length;
220
+ const appCount = resolved.length - sharedCount;
221
+ console.log(
222
+ `[verify-contracts] api=${args.api}: ${resolved.length} operations ` +
223
+ `(${sharedCount} shared, ${appCount} app-local override)`,
224
+ );
232
225
 
233
226
  const state = buildSeededState();
234
227
  const executor: Executor =
@@ -239,16 +232,9 @@ async function main(): Promise<void> {
239
232
  let skipped = 0;
240
233
  const failures: { contract: string; diff: string }[] = [];
241
234
 
242
- for (const f of contractFiles) {
243
- const path = resolve(args.contractsDir, f);
244
- let st;
245
- try {
246
- st = statSync(path);
247
- } catch {
248
- continue;
249
- }
250
- if (!st.isFile()) continue;
251
- const contract = JSON.parse(readFileSync(path, 'utf8')) as Contract;
235
+ for (const entry of resolved) {
236
+ const contract = entry.contract;
237
+ const label = `${entry.operationName} [${entry.origin}]`;
252
238
  // Contracts captured with a warning never executed cleanly;
253
239
  // skip them in verify (they need fixtures.json before they can
254
240
  // be verified).
@@ -262,7 +248,7 @@ async function main(): Promise<void> {
262
248
  } catch (err) {
263
249
  drift++;
264
250
  failures.push({
265
- contract: f,
251
+ contract: label,
266
252
  diff: `executor threw: ${(err as Error).message}`,
267
253
  });
268
254
  continue;
@@ -280,7 +266,7 @@ async function main(): Promise<void> {
280
266
  pass++;
281
267
  } else {
282
268
  drift++;
283
- failures.push({ contract: f, diff: d });
269
+ failures.push({ contract: label, diff: d });
284
270
  }
285
271
  }
286
272
 
@@ -291,9 +277,7 @@ async function main(): Promise<void> {
291
277
  console.log('');
292
278
  console.log('[verify-contracts] drift:');
293
279
  for (const f of failures) {
294
- console.log(
295
- ` ${relative(args.cwd, resolve(args.contractsDir, f.contract))}`,
296
- );
280
+ console.log(` ${f.contract}`);
297
281
  console.log(` ${f.diff}`);
298
282
  }
299
283
  console.log('');
@@ -20,8 +20,11 @@
20
20
  * GraphQL verifier uses. Offline-offline matches byte-for-byte
21
21
  * anyway; the normaliser only matters for live-vs-contract.
22
22
  */
23
- import { readdirSync, readFileSync, statSync } from 'node:fs';
24
- import { resolve, relative } from 'node:path';
23
+ import { resolve } from 'node:path';
24
+ import {
25
+ resolveRestContracts,
26
+ type RestContract,
27
+ } from '@essential-apps/shopify-test-contracts/operation-contract';
25
28
  import {
26
29
  createAdminApi,
27
30
  } from '@essential-apps/shopify-test-shopify-api';
@@ -33,21 +36,7 @@ interface Args {
33
36
  cwd: string;
34
37
  }
35
38
 
36
- interface Contract {
37
- operationName: string;
38
- protocol: 'rest';
39
- method: 'GET' | 'POST' | 'PUT' | 'DELETE';
40
- path: string;
41
- pathParams?: Record<string, string>;
42
- query?: Record<string, string>;
43
- body?: unknown;
44
- response: {
45
- status: number;
46
- body: unknown;
47
- };
48
- capturedFrom: 'offline';
49
- warning?: string;
50
- }
39
+ type Contract = RestContract;
51
40
 
52
41
  const ADMIN_API_VERSION = '2025-07';
53
42
 
@@ -173,21 +162,20 @@ function diff(
173
162
 
174
163
  async function main(): Promise<void> {
175
164
  const args = parseArgs();
176
- let entries: string[];
177
- try {
178
- entries = readdirSync(args.contractsDir);
179
- } catch {
165
+ // Merged view: shared package REST goldens overlaid by the app's
166
+ // local admin-rest dir (app wins by operation name).
167
+ const resolved = resolveRestContracts({ appDir: args.contractsDir });
168
+ if (resolved.length === 0) {
180
169
  console.error(
181
- `[verify-rest] no contracts directory at ${args.contractsDir}. ` +
182
- `Run \`npm run test:online:capture-rest-contracts\` first.`,
170
+ `[verify-rest] no REST contracts (shared package + ${args.contractsDir}).`,
183
171
  );
184
172
  process.exit(2);
185
173
  }
186
- const contractFiles = entries.filter((f) => f.endsWith('.json'));
187
- if (contractFiles.length === 0) {
188
- console.error(`[verify-rest] no contracts found in ${args.contractsDir}`);
189
- process.exit(2);
190
- }
174
+ const sharedCount = resolved.filter((r) => r.origin === 'shared').length;
175
+ console.log(
176
+ `[verify-rest] ${resolved.length} operations ` +
177
+ `(${sharedCount} shared, ${resolved.length - sharedCount} app-local override)`,
178
+ );
191
179
 
192
180
  const state = buildSeededState();
193
181
  const executor: RestExecutor =
@@ -198,16 +186,9 @@ async function main(): Promise<void> {
198
186
  let skipped = 0;
199
187
  const failures: { contract: string; diff: string }[] = [];
200
188
 
201
- for (const f of contractFiles) {
202
- const path = resolve(args.contractsDir, f);
203
- let st;
204
- try {
205
- st = statSync(path);
206
- } catch {
207
- continue;
208
- }
209
- if (!st.isFile()) continue;
210
- const contract = JSON.parse(readFileSync(path, 'utf8')) as Contract;
189
+ for (const entry of resolved) {
190
+ const contract = entry.contract as Contract;
191
+ const label = `${entry.operationName} [${entry.origin}]`;
211
192
  if (contract.warning) {
212
193
  skipped++;
213
194
  continue;
@@ -217,14 +198,14 @@ async function main(): Promise<void> {
217
198
  actual = await executor(contract);
218
199
  } catch (err) {
219
200
  drift++;
220
- failures.push({ contract: f, diff: `executor threw: ${(err as Error).message}` });
201
+ failures.push({ contract: label, diff: `executor threw: ${(err as Error).message}` });
221
202
  continue;
222
203
  }
223
204
  // Status-code mismatch is a hard diff — surface it directly.
224
205
  if (actual.status !== contract.response.status) {
225
206
  drift++;
226
207
  failures.push({
227
- contract: f,
208
+ contract: label,
228
209
  diff: `$.status: expected ${contract.response.status}, got ${actual.status}`,
229
210
  });
230
211
  continue;
@@ -238,7 +219,7 @@ async function main(): Promise<void> {
238
219
  pass++;
239
220
  } else {
240
221
  drift++;
241
- failures.push({ contract: f, diff: d });
222
+ failures.push({ contract: label, diff: d });
242
223
  }
243
224
  }
244
225
 
@@ -249,7 +230,7 @@ async function main(): Promise<void> {
249
230
  console.log('');
250
231
  console.log('[verify-rest] drift:');
251
232
  for (const f of failures) {
252
- console.log(` ${relative(args.cwd, resolve(args.contractsDir, f.contract))}`);
233
+ console.log(` ${f.contract}`);
253
234
  console.log(` ${f.diff}`);
254
235
  }
255
236
  console.log('');
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=nodeShim.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"nodeShim.d.ts","sourceRoot":"","sources":["../../src/edge/nodeShim.ts"],"names":[],"mappings":""}