@bongos/core 1.19.710 → 1.19.712

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.
@@ -57,7 +57,7 @@ t('previewUrl: reads box.env via injected fs; null when unreadable', () => {
57
57
 
58
58
  // ---------- previewable surfaces ----------
59
59
  t('isPreviewableFile: game surfaces yes, internal surfaces no', () => {
60
- for (const f of ['public/game/main.js', 'src/world/terrain.js', 'src/rooms/WorldRoom.js', 'server.js']) {
60
+ for (const f of ['modules/game/public/game/main.js', 'modules/game/world/terrain.js', 'modules/game/rooms/WorldRoom.js', 'server.js']) {
61
61
  assert.equal(sbx.isPreviewableFile(f), true, `${f} should be previewable`);
62
62
  }
63
63
  for (const f of ['scripts/gds/ship.js', 'modules/dev-box/routes/box.js', 'docs/adr/0046.md', 'modules/status-ui/public/index.html', 'modules/hall-ui/public/hall.js', 'migrations/075.sql', 'tests/sandbox_stage.mjs', 'server.js.bak']) {
@@ -66,7 +66,7 @@ t('isPreviewableFile: game surfaces yes, internal surfaces no', () => {
66
66
  });
67
67
 
68
68
  t('anyPreviewable: mixed diff counts as previewable; empty/none does not', () => {
69
- assert.equal(sbx.anyPreviewable(['docs/x.md', 'public/game/a.js']), true);
69
+ assert.equal(sbx.anyPreviewable(['docs/x.md', 'modules/game/public/game/a.js']), true);
70
70
  assert.equal(sbx.anyPreviewable(['docs/x.md', 'scripts/gds/y.js']), false);
71
71
  assert.equal(sbx.anyPreviewable([]), false);
72
72
  assert.equal(sbx.anyPreviewable(null), false);
@@ -162,9 +162,9 @@ function git(...args) { execFileSync('git', args, { cwd: tmp, encoding: 'utf8' }
162
162
  git('init', '-q');
163
163
  git('config', 'user.email', 'test@example.com');
164
164
  git('config', 'user.name', 'test');
165
- fs.mkdirSync(path.join(tmp, 'public'));
165
+ fs.mkdirSync(path.join(tmp, 'modules', 'game', 'public'), { recursive: true });
166
166
  fs.mkdirSync(path.join(tmp, 'docs'));
167
- fs.writeFileSync(path.join(tmp, 'public', 'game.js'), 'one\n');
167
+ fs.writeFileSync(path.join(tmp, 'modules', 'game', 'public', 'game.js'), 'one\n');
168
168
  fs.writeFileSync(path.join(tmp, 'server.js'), 'srv\n');
169
169
  fs.writeFileSync(path.join(tmp, 'docs', 'notes.md'), 'doc\n');
170
170
  git('add', '.');
@@ -181,7 +181,7 @@ t('computeTreeStamp: content-stable, scoped to game surfaces', () => {
181
181
  assert.equal(sbx.computeTreeStamp({ cwd: tmp }), s1, 'docs edit → stamp unchanged');
182
182
 
183
183
  // A game-surface edit produces a new stamp...
184
- fs.writeFileSync(path.join(tmp, 'public', 'game.js'), 'two\n');
184
+ fs.writeFileSync(path.join(tmp, 'modules', 'game', 'public', 'game.js'), 'two\n');
185
185
  const s3 = sbx.computeTreeStamp({ cwd: tmp });
186
186
  assert.notEqual(s3, s1, 'game edit → new stamp');
187
187
 
@@ -192,10 +192,10 @@ t('computeTreeStamp: content-stable, scoped to game surfaces', () => {
192
192
  assert.equal(sbx.computeTreeStamp({ cwd: tmp }), s3, 'commit of same content → stamp unchanged');
193
193
 
194
194
  // An untracked game file counts.
195
- fs.writeFileSync(path.join(tmp, 'public', 'new-tile.js'), 'untracked\n');
195
+ fs.writeFileSync(path.join(tmp, 'modules', 'game', 'public', 'new-tile.js'), 'untracked\n');
196
196
  const s4 = sbx.computeTreeStamp({ cwd: tmp });
197
197
  assert.notEqual(s4, s3, 'untracked game file → new stamp');
198
- fs.rmSync(path.join(tmp, 'public', 'new-tile.js'));
198
+ fs.rmSync(path.join(tmp, 'modules', 'game', 'public', 'new-tile.js'));
199
199
  assert.equal(sbx.computeTreeStamp({ cwd: tmp }), s3, 'removing it restores the stamp');
200
200
 
201
201
  assert.equal(sbx.computeTreeStamp({ cwd: os.tmpdir() }), null, 'not a repo → null');
@@ -265,7 +265,7 @@ await ta('runShipGate: no sandbox available (not a box, no local game entry) →
265
265
  // is present — a partial clone with no game. (A full local clone WITH the game
266
266
  // is covered by the LOCAL tests below, where it DOES stage.)
267
267
  const r = await sbx.runShipGate(
268
- { taskId: 1, files: ['public/game/a.js'], skipStage: false, tty: true },
268
+ { taskId: 1, files: ['modules/game/public/game/a.js'], skipStage: false, tty: true },
269
269
  { fs: { existsSync: () => false } }
270
270
  );
271
271
  assert.equal(r.ok, true);
@@ -281,7 +281,7 @@ await ta('runShipGate: box + non-game diff → ok, no staging', async () => {
281
281
  await ta('runShipGate: box + game diff + TTY, reviewer says yes → stages then ok, records tty sign-off', async () => {
282
282
  const mp = path.join(markerDir, 'm2.json');
283
283
  const deps = boxDeps(mp, { promptYesNo: async () => true });
284
- const r = await sbx.runShipGate({ taskId: 927, files: ['src/world/terrain.js'], skipStage: false, tty: true }, deps);
284
+ const r = await sbx.runShipGate({ taskId: 927, files: ['modules/game/world/terrain.js'], skipStage: false, tty: true }, deps);
285
285
  assert.equal(r.ok, true);
286
286
  const m = JSON.parse(fs.readFileSync(mp, 'utf8'));
287
287
  assert.equal(m.url, 'https://sandbox-test.example.com');
@@ -295,7 +295,7 @@ await ta('runShipGate: box + game diff + TTY, reviewer says yes → stages then
295
295
  await ta('runShipGate: reviewer says NO → aborts with claim-preserving reason', async () => {
296
296
  const mp = path.join(markerDir, 'm3.json');
297
297
  const deps = boxDeps(mp, { promptYesNo: async () => false });
298
- const r = await sbx.runShipGate({ taskId: 927, files: ['public/game/a.js'], skipStage: false, tty: true }, deps);
298
+ const r = await sbx.runShipGate({ taskId: 927, files: ['modules/game/public/game/a.js'], skipStage: false, tty: true }, deps);
299
299
  assert.equal(r.ok, false);
300
300
  assert.match(r.reason, /sandbox review declined/);
301
301
  });
@@ -321,14 +321,14 @@ await ta('runShipGate: stale marker forces a re-stage (stamp mismatch)', async (
321
321
  await ta('runShipGate: non-TTY → stages then ABORTS with the review URL (no prompt, no hang)', async () => {
322
322
  const mp = path.join(markerDir, 'm6.json');
323
323
  const deps = boxDeps(mp, { promptYesNo: async () => { throw new Error('must not prompt'); } });
324
- const r = await sbx.runShipGate({ taskId: 927, files: ['public/game/a.js'], skipStage: false, tty: false }, deps);
324
+ const r = await sbx.runShipGate({ taskId: 927, files: ['modules/game/public/game/a.js'], skipStage: false, tty: false }, deps);
325
325
  assert.equal(r.ok, false, 'unreviewed game work must not ship non-interactively');
326
326
  assert.equal(r.review, true, 'abort is flagged as a review pause');
327
327
  assert.match(r.reason, /staged at https:\/\/sandbox-test\.example\.com/i);
328
328
  assert.ok(fs.existsSync(mp), 'work is staged so the human can review at the URL');
329
329
  // ...and after the (notional) review, a bare re-run passes on the fresh marker
330
330
  // but records NO sign-off (no --approved) — back-compat / failure-open.
331
- const r2 = await sbx.runShipGate({ taskId: 927, files: ['public/game/a.js'], skipStage: false, tty: false }, deps);
331
+ const r2 = await sbx.runShipGate({ taskId: 927, files: ['modules/game/public/game/a.js'], skipStage: false, tty: false }, deps);
332
332
  assert.equal(r2.ok, true, 're-run after staging passes (marker fresh)');
333
333
  assert.equal(r2.sandboxReview, undefined, 'bare re-run records no sign-off');
334
334
  });
@@ -336,16 +336,16 @@ await ta('runShipGate: non-TTY → stages then ABORTS with the review URL (no pr
336
336
  await ta('runShipGate: uncommitted game edit counts as previewable even when committed diff does not', async () => {
337
337
  const mp = path.join(markerDir, 'm9.json');
338
338
  const deps = boxDeps(mp, { promptYesNo: async () => true });
339
- fs.writeFileSync(path.join(tmp, 'public', 'game.js'), 'dirty working tree\n');
339
+ fs.writeFileSync(path.join(tmp, 'modules', 'game', 'public', 'game.js'), 'dirty working tree\n');
340
340
  const r = await sbx.runShipGate({ taskId: 927, files: ['docs/x.md'], skipStage: false, tty: true }, deps);
341
341
  assert.equal(r.ok, true);
342
342
  assert.ok(fs.existsSync(mp), 'gate staged the dirty game surface');
343
- git('checkout', '--', 'public/game.js');
343
+ git('checkout', '--', 'modules/game/public/game.js');
344
344
  });
345
345
 
346
346
  await ta('runShipGate: --skip-stage → ok, untouched', async () => {
347
347
  const mp = path.join(markerDir, 'm7.json');
348
- const r = await sbx.runShipGate({ taskId: 927, files: ['public/game/a.js'], skipStage: true, tty: true }, boxDeps(mp));
348
+ const r = await sbx.runShipGate({ taskId: 927, files: ['modules/game/public/game/a.js'], skipStage: true, tty: true }, boxDeps(mp));
349
349
  assert.equal(r.ok, true);
350
350
  assert.equal(fs.existsSync(mp), false);
351
351
  });
@@ -357,7 +357,7 @@ await ta('runShipGate: --skip-stage → ok, untouched', async () => {
357
357
  await ta('1002607 runShipGate: staging failure (health never green) ABORTS — review gate cannot silently turn itself off', async () => {
358
358
  const mp = path.join(markerDir, 'm8.json');
359
359
  const deps = boxDeps(mp, { fetchImpl: async () => ({ status: 502 }), timeoutMs: 10 });
360
- const r = await sbx.runShipGate({ taskId: 927, files: ['public/game/a.js'], skipStage: false, tty: true }, deps);
360
+ const r = await sbx.runShipGate({ taskId: 927, files: ['modules/game/public/game/a.js'], skipStage: false, tty: true }, deps);
361
361
  assert.equal(r.ok, false, 'a failed stage must not ship as if the review happened');
362
362
  assert.equal(r.review, undefined, 'a failure to present is NOT a calm review pause');
363
363
  assert.match(r.reason, /DID NOT run/, 'says plainly the review step did not run');
@@ -384,7 +384,7 @@ await ta('runShipGate: --approved cannot pre-approve an UNstaged change (still s
384
384
  const deps = boxDeps(mp, { promptYesNo: async () => { throw new Error('must not prompt'); } });
385
385
  // No marker → not staged-fresh → --approved is ignored; the gate still stages
386
386
  // + aborts for review. You cannot sign off on something never shown.
387
- const r = await sbx.runShipGate({ taskId: 1048, files: ['public/game/a.js'], skipStage: false, tty: false, approved: true }, deps);
387
+ const r = await sbx.runShipGate({ taskId: 1048, files: ['modules/game/public/game/a.js'], skipStage: false, tty: false, approved: true }, deps);
388
388
  assert.equal(r.ok, false, '--approved must not bypass the first stage + review');
389
389
  assert.equal(r.review, true);
390
390
  assert.equal(r.sandboxReview, undefined, 'nothing recorded — nothing was staged to approve');
@@ -402,13 +402,13 @@ await ta('syncWorktreeToServed: brings the branch content into the served checko
402
402
  const g = (...a) => execFileSync('git', a, { cwd: served, encoding: 'utf8' });
403
403
  g('init', '-q');
404
404
  g('config', 'user.email', 't@e.com'); g('config', 'user.name', 't');
405
- fs.mkdirSync(path.join(served, 'public'));
406
- fs.writeFileSync(path.join(served, 'public', 'game.js'), 'MAIN content\n');
405
+ fs.mkdirSync(path.join(served, 'modules', 'game', 'public'), { recursive: true });
406
+ fs.writeFileSync(path.join(served, 'modules', 'game', 'public', 'game.js'), 'MAIN content\n');
407
407
  g('add', '.'); g('commit', '-qm', 'main');
408
408
  // a linked worktree on a feature branch with different game content
409
409
  const wt = fs.mkdtempSync(path.join(os.tmpdir(), 'otb-wt-')) + '/co';
410
410
  g('worktree', 'add', '-q', '-b', 'claude/feat', wt);
411
- fs.writeFileSync(path.join(wt, 'public', 'game.js'), 'BRANCH content\n');
411
+ fs.writeFileSync(path.join(wt, 'modules', 'game', 'public', 'game.js'), 'BRANCH content\n');
412
412
  execFileSync('git', ['add', '.'], { cwd: wt });
413
413
  execFileSync('git', ['commit', '-qm', 'branch edit'], { cwd: wt });
414
414
 
@@ -419,7 +419,7 @@ await ta('syncWorktreeToServed: brings the branch content into the served checko
419
419
  // The claim is WHICH content landed, not how its lines end: git checks the fixture
420
420
  // out under the machine's own autocrlf, so on Windows the file comes back CRLF and a
421
421
  // byte-exact compare failed for a reason unrelated to the sync (task 1003529).
422
- assert.ok(sameContent(fs.readFileSync(path.join(served, 'public', 'game.js'), 'utf8'), 'BRANCH content\n'),
422
+ assert.ok(sameContent(fs.readFileSync(path.join(served, 'modules', 'game', 'public', 'game.js'), 'utf8'), 'BRANCH content\n'),
423
423
  'served /workspace now shows the SESSION branch content, not stale main');
424
424
 
425
425
  g('worktree', 'remove', '--force', wt);
@@ -469,26 +469,26 @@ t('syncWorktreeToServed: detached HEAD / unresolvable branch → not ok', () =>
469
469
 
470
470
  await ta('verifyServedFresh: served bytes match worktree → ok/verified', async () => {
471
471
  // dirty a tracked game file so pickVerifyTarget selects it
472
- fs.writeFileSync(path.join(tmp, 'public', 'game.js'), 'served-match\n');
472
+ fs.writeFileSync(path.join(tmp, 'modules', 'game', 'public', 'game.js'), 'served-match\n');
473
473
  const r = await sbx.verifyServedFresh({
474
474
  cwd: tmp,
475
475
  healthUrl: 'http://127.0.0.1:3000/',
476
476
  fetchImpl: async (url) => { assert.match(url, /\/game\.js$/); return { status: 200, text: async () => 'served-match\n' }; },
477
477
  });
478
478
  assert.equal(r.ok, true);
479
- assert.equal(r.verified, 'public/game.js');
480
- execFileSync('git', ['checkout', '--', 'public/game.js'], { cwd: tmp });
479
+ assert.equal(r.verified, 'modules/game/public/game.js');
480
+ execFileSync('git', ['checkout', '--', 'modules/game/public/game.js'], { cwd: tmp });
481
481
  });
482
482
 
483
483
  await ta('verifyServedFresh: served bytes DIFFER → not ok (serving stale code)', async () => {
484
- fs.writeFileSync(path.join(tmp, 'public', 'game.js'), 'worktree-new\n');
484
+ fs.writeFileSync(path.join(tmp, 'modules', 'game', 'public', 'game.js'), 'worktree-new\n');
485
485
  const r = await sbx.verifyServedFresh({
486
486
  cwd: tmp,
487
487
  fetchImpl: async () => ({ status: 200, text: async () => 'stale-old\n' }),
488
488
  });
489
489
  assert.equal(r.ok, false);
490
490
  assert.match(r.reason, /serving stale code/);
491
- execFileSync('git', ['checkout', '--', 'public/game.js'], { cwd: tmp });
491
+ execFileSync('git', ['checkout', '--', 'modules/game/public/game.js'], { cwd: tmp });
492
492
  });
493
493
 
494
494
  await ta('verifyServedFresh: no served static change → skipped (ok)', async () => {
@@ -506,7 +506,7 @@ await ta('stage(box): a HARD sync failure → {ok:false,hard:true}, no marker; r
506
506
  assert.equal(staged.hard, true);
507
507
  assert.equal(fs.existsSync(mp), false, 'no marker on a hard sync failure');
508
508
  // and the gate turns a hard failure into a claim-preserving abort
509
- const r = await sbx.runShipGate({ taskId: 1068, files: ['public/game/a.js'], skipStage: false, tty: true }, deps);
509
+ const r = await sbx.runShipGate({ taskId: 1068, files: ['modules/game/public/game/a.js'], skipStage: false, tty: true }, deps);
510
510
  assert.equal(r.ok, false);
511
511
  assert.equal(r.review, true);
512
512
  assert.match(r.reason, /could not be trusted/);
@@ -526,7 +526,7 @@ await ta('1002607 runShipGate(box): a SOFT failure (preview never healthy) now f
526
526
  // sync ok (injected), but health never green → the ship must NOT continue
527
527
  // as if the review ran (that was the silent-disable defect).
528
528
  const deps = boxDeps(mp, { fetchImpl: async () => ({ status: 502 }), timeoutMs: 10 });
529
- const r = await sbx.runShipGate({ taskId: 1068, files: ['public/game/a.js'], skipStage: false, tty: true }, deps);
529
+ const r = await sbx.runShipGate({ taskId: 1068, files: ['modules/game/public/game/a.js'], skipStage: false, tty: true }, deps);
530
530
  assert.equal(r.ok, false, 'the soft failure aborts instead of silently skipping the review');
531
531
  assert.match(r.reason, /sandbox staging failed/);
532
532
  });
@@ -610,7 +610,7 @@ function localDeps(markerFile, overrides = {}) {
610
610
 
611
611
  await ta('runShipGate(local): game diff + non-TTY → stages then ABORTS with the localhost URL', async () => {
612
612
  const mp = path.join(markerDir, 'lm1.json');
613
- const r = await sbx.runShipGate({ taskId: 1056, files: ['public/game/a.js'], skipStage: false, tty: false }, localDeps(mp));
613
+ const r = await sbx.runShipGate({ taskId: 1056, files: ['modules/game/public/game/a.js'], skipStage: false, tty: false }, localDeps(mp));
614
614
  assert.equal(r.ok, false, 'unreviewed game work must not ship non-interactively on a laptop');
615
615
  assert.equal(r.review, true);
616
616
  assert.match(r.reason, /staged at http:\/\/localhost:3100/i);
@@ -619,7 +619,7 @@ await ta('runShipGate(local): game diff + non-TTY → stages then ABORTS with th
619
619
 
620
620
  await ta('runShipGate(local): TTY yes → records a tty sign-off with the localhost URL + kind=local marker', async () => {
621
621
  const mp = path.join(markerDir, 'lm2.json');
622
- const r = await sbx.runShipGate({ taskId: 1056, files: ['src/world/terrain.js'], skipStage: false, tty: true }, localDeps(mp, { promptYesNo: async () => true }));
622
+ const r = await sbx.runShipGate({ taskId: 1056, files: ['modules/game/world/terrain.js'], skipStage: false, tty: true }, localDeps(mp, { promptYesNo: async () => true }));
623
623
  assert.equal(r.ok, true);
624
624
  assert.equal(r.sandboxReview.method, 'tty');
625
625
  assert.equal(r.sandboxReview.url, 'http://localhost:3100');
@@ -648,5 +648,86 @@ await ta('runShipGate(local): non-game diff (clean tree) → skip, no staging',
648
648
 
649
649
  fs.rmSync(tmp, { recursive: true, force: true });
650
650
 
651
+ // ---------- 1002062: the surface list must describe a REAL tree ----------
652
+ //
653
+ // WHY THESE EXIST. Every case above feeds sandbox-stage its own fixture paths,
654
+ // so the previewable list matched NOTHING in the real repo for 69 days and all
655
+ // 47 tests stayed green. A suite that supplies the paths it tests cannot notice
656
+ // that the paths are wrong. These four look at the real thing instead.
657
+
658
+ t('1002062 the three surface spellings are DERIVED, so they cannot drift apart', () => {
659
+ // They used to be three hand-maintained copies. The module carve updated none
660
+ // of them, and nothing compared them. Derivation is the fix; this pins it.
661
+ assert.deepEqual(
662
+ sbx.PREVIEWABLE_PATHSPECS,
663
+ [...sbx.PREVIEWABLE_DIRS, ...sbx.PREVIEWABLE_ENTRYPOINTS],
664
+ "PATHSPECS must be derived from DIRS + ENTRYPOINTS, not hand-listed"
665
+ );
666
+ for (const d of sbx.PREVIEWABLE_DIRS) {
667
+ assert.equal(sbx.isPreviewableFile(`${d}/some/file.js`), true, `${d} must match via the derived prefixes`);
668
+ }
669
+ for (const e of sbx.PREVIEWABLE_ENTRYPOINTS) {
670
+ assert.equal(sbx.isPreviewableFile(e), true, `${e} must match via the derived file set`);
671
+ }
672
+ });
673
+
674
+ t('1002062 no PRE-CARVE relic survives in the surface list', () => {
675
+ // The exact spellings that were stale: the carve (ADR 0083 / task 1418) moved
676
+ // them under modules/game/ and this list kept the old ones.
677
+ for (const relic of ['public', 'src/world', 'src/rooms']) {
678
+ assert.ok(
679
+ !sbx.PREVIEWABLE_PATHSPECS.includes(relic),
680
+ `${relic} is the pre-carve spelling and matches nothing — it must not be back`
681
+ );
682
+ }
683
+ assert.ok(sbx.PREVIEWABLE_STATIC_DIR.startsWith('modules/'), 'the served static dir is module-carved');
684
+ });
685
+
686
+ t('1002062 a client directory that matched NOTHING is reported, not swallowed', () => {
687
+ // THE ACTUAL BUG. The sync collapsed to one boolean, so a run that synced only
688
+ // the entrypoints and missed every client dir reported plain success — the
689
+ // reporter saw "synced previewable surfaces" and got a stale sandbox.
690
+ const served = fs.mkdtempSync(path.join(os.tmpdir(), "sbx-missed-"));
691
+ const session = fs.mkdtempSync(path.join(os.tmpdir(), "sbx-sess-"));
692
+ const calls = [];
693
+ const execFileSyncStub = (bin, args) => {
694
+ const a = args.join(" ");
695
+ if (a.includes("rev-parse")) return "feature-branch\n";
696
+ // Entrypoints resolve; every client directory is absent from the branch.
697
+ const ps = args[args.length - 1];
698
+ if (sbx.PREVIEWABLE_DIRS.includes(ps) && a.includes("feature-branch")) {
699
+ throw new Error(`pathspec ${ps} did not match any file(s) known to git`);
700
+ }
701
+ calls.push(a);
702
+ return "";
703
+ };
704
+ const r = sbx.syncWorktreeToServed({
705
+ execFileSync: execFileSyncStub,
706
+ servedRoot: served,
707
+ sessionRoot: session,
708
+ cwd: session,
709
+ });
710
+ assert.equal(r.ok, true, "the entrypoints synced, so the sync itself is not a hard failure");
711
+ assert.deepEqual(
712
+ r.missedDirs, sbx.PREVIEWABLE_DIRS,
713
+ "every client directory missed, and the caller must be told which"
714
+ );
715
+ });
716
+
717
+ t('1002062 pickVerifyTarget maps the CARVED static dir onto the served URL', () => {
718
+ // modules/game/public/game.js is served at /game.js. The old code stripped a
719
+ // hardcoded "public/" prefix, which post-carve would have left the whole
720
+ // modules/game/ path in the URL.
721
+ const rel = `${sbx.PREVIEWABLE_STATIC_DIR}/game.js`;
722
+ const target = sbx.pickVerifyTarget({
723
+ cwd: "/nowhere",
724
+ fs: { existsSync: () => true },
725
+ execFileSync: (bin, args) => (args.includes("--porcelain") ? ` M ${rel}\n` : ""),
726
+ });
727
+ assert.ok(target, "a changed file under the static dir must be pickable");
728
+ assert.equal(target.relPath, rel);
729
+ assert.equal(target.urlPath, "/game.js", "the carved prefix is stripped, not just \"public/\"");
730
+ });
731
+
651
732
  console.log(`\nsandbox_stage: ${passed} passed, ${failed} failed`);
652
733
  process.exit(failed ? 1 : 0);