@elench/testkit 0.1.13 → 0.1.15

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/lib/runner.mjs CHANGED
@@ -3,12 +3,15 @@ import path from "path";
3
3
  import { spawn } from "child_process";
4
4
  import net from "net";
5
5
  import { execa, execaCommand } from "execa";
6
- import { runScript } from "./exec.mjs";
6
+ import { resolveDalBinary, resolveServiceCwd } from "./config.mjs";
7
7
  import {
8
- requireNeonApiKey,
9
- resolveDalBinary,
10
- resolveServiceCwd,
11
- } from "./config.mjs";
8
+ cleanupOrphanedLocalInfrastructure,
9
+ destroyRuntimeDatabase,
10
+ destroyServiceDatabaseCache,
11
+ isDatabaseStateDir,
12
+ prepareDatabaseRuntime,
13
+ showServiceDatabaseStatus,
14
+ } from "./database.mjs";
12
15
 
13
16
  const TYPE_ORDER = ["dal", "integration", "e2e", "load"];
14
17
  const HTTP_K6_TYPES = new Set(["integration", "e2e", "load"]);
@@ -18,6 +21,7 @@ const PORT_STRIDE = 100;
18
21
  export async function runAll(configs, suiteType, suiteNames, opts, allConfigs = configs) {
19
22
  const configMap = new Map(allConfigs.map((config) => [config.name, config]));
20
23
  const targetSpan = Math.max(1, opts.jobs || 1);
24
+ const startedAt = Date.now();
21
25
  const results = await Promise.all(
22
26
  configs.map(async (config, targetSlot) => {
23
27
  console.log(`\n══ ${config.name} ══`);
@@ -28,7 +32,8 @@ export async function runAll(configs, suiteType, suiteNames, opts, allConfigs =
28
32
  })
29
33
  );
30
34
 
31
- if (results.some(Boolean)) process.exit(1);
35
+ printRunSummary(results, Date.now() - startedAt);
36
+ if (results.some((result) => result.failed)) process.exit(1);
32
37
  }
33
38
 
34
39
  export async function destroy(config) {
@@ -36,28 +41,28 @@ export async function destroy(config) {
36
41
 
37
42
  const runtimeStateDirs = findRuntimeStateDirs(config.stateDir);
38
43
  for (const stateDir of runtimeStateDirs) {
39
- const projectId = readStateValue(path.join(stateDir, "neon_project_id"));
40
- if (!projectId) continue;
41
-
42
- await runScript("neon-down.sh", {
43
- NEON_PROJECT_ID: projectId,
44
- STATE_DIR: stateDir,
44
+ await destroyRuntimeDatabase({
45
+ productDir: config.productDir,
46
+ stateDir,
45
47
  });
46
48
  }
47
49
 
50
+ await destroyServiceDatabaseCache(config.productDir, config.name);
48
51
  fs.rmSync(config.stateDir, { recursive: true, force: true });
52
+ await cleanupOrphanedLocalInfrastructure(config.productDir);
49
53
  }
50
54
 
51
55
  export function showStatus(config) {
52
56
  if (!fs.existsSync(config.stateDir)) {
53
57
  console.log("No state — run tests first.");
54
- return;
58
+ } else {
59
+ printStateDir(config.stateDir, " ");
55
60
  }
56
-
57
- printStateDir(config.stateDir, " ");
61
+ showServiceDatabaseStatus(config.productDir, config.name);
58
62
  }
59
63
 
60
64
  async function runService(targetConfig, configMap, suiteType, suiteNames, opts, runtimeSlot) {
65
+ const startedAt = Date.now();
61
66
  const suites = applyShard(
62
67
  collectSuites(targetConfig, suiteType, suiteNames, opts.framework),
63
68
  opts.shard
@@ -66,7 +71,17 @@ async function runService(targetConfig, configMap, suiteType, suiteNames, opts,
66
71
  console.log(
67
72
  `No test files for ${targetConfig.name} type="${suiteType}" suites=${suiteNames.join(",") || "all"} — skipping`
68
73
  );
69
- return false;
74
+ return {
75
+ name: targetConfig.name,
76
+ failed: false,
77
+ skipped: true,
78
+ suiteCount: 0,
79
+ completedSuiteCount: 0,
80
+ failedSuiteCount: 0,
81
+ durationMs: Date.now() - startedAt,
82
+ workers: [],
83
+ errors: [],
84
+ };
70
85
  }
71
86
 
72
87
  const runtimeConfigs = resolveRuntimeConfigs(targetConfig, configMap);
@@ -83,17 +98,35 @@ async function runService(targetConfig, configMap, suiteType, suiteNames, opts,
83
98
 
84
99
  const results = await Promise.allSettled(workerPlans.map((plan) => runWorkerPlan(plan)));
85
100
  let failed = false;
101
+ const workers = [];
102
+ const errors = [];
103
+ let failedSuiteCount = 0;
104
+ let completedSuiteCount = 0;
86
105
 
87
106
  for (const result of results) {
88
107
  if (result.status === "rejected") {
89
108
  failed = true;
90
109
  console.error(result.reason);
110
+ errors.push(formatError(result.reason));
91
111
  continue;
92
112
  }
93
- if (result.value) failed = true;
113
+ workers.push(result.value);
114
+ completedSuiteCount += result.value.completedSuiteCount;
115
+ failedSuiteCount += result.value.failedSuiteCount;
116
+ if (result.value.failed) failed = true;
94
117
  }
95
118
 
96
- return failed;
119
+ return {
120
+ name: targetConfig.name,
121
+ failed,
122
+ skipped: false,
123
+ suiteCount: suites.length,
124
+ completedSuiteCount,
125
+ failedSuiteCount,
126
+ durationMs: Date.now() - startedAt,
127
+ workers,
128
+ errors,
129
+ };
97
130
  }
98
131
 
99
132
  function collectSuites(config, suiteType, suiteNames, frameworkFilter) {
@@ -356,9 +389,12 @@ function resolveWorkerConfig(
356
389
  ? {
357
390
  ...config.testkit.database,
358
391
  branchName:
392
+ config.testkit.database.provider === "neon" &&
359
393
  config.testkit.database.branchName !== undefined
360
394
  ? finalizeString(config.testkit.database.branchName, context)
361
- : `${targetConfig.name}-${config.name}-w${workerId}-testkit`,
395
+ : config.testkit.database.provider === "neon"
396
+ ? `${targetConfig.name}-${config.name}-w${workerId}-testkit`
397
+ : undefined,
362
398
  }
363
399
  : undefined;
364
400
 
@@ -421,17 +457,18 @@ function resolveWorkerConfig(
421
457
  }
422
458
 
423
459
  async function runWorkerPlan(plan) {
460
+ const startedAt = Date.now();
424
461
  console.log(
425
462
  `\n══ ${plan.targetConfig.name} worker ${plan.workerId} (${plan.suites.length} suite${plan.suites.length === 1 ? "" : "s"}) ══`
426
463
  );
427
464
 
428
465
  let startedServices = [];
429
466
  let failed = false;
467
+ const suiteResults = [];
468
+ let fatalError = null;
430
469
 
431
470
  try {
432
471
  await prepareDatabases(plan.runtimeConfigs);
433
- await runMigrations(plan.runtimeConfigs);
434
- await runSeeds(plan.runtimeConfigs);
435
472
 
436
473
  if (needsLocalRuntime(plan.suites)) {
437
474
  startedServices = await startLocalServices(plan.runtimeConfigs);
@@ -442,71 +479,70 @@ async function runWorkerPlan(plan) {
442
479
  `\n── ${plan.targetConfig.workerLabel} ${suite.type}:${suite.name} (${suite.framework}) ──`
443
480
  );
444
481
  const result = await runSuite(plan.targetConfig, suite);
482
+ suiteResults.push(result);
445
483
  if (result.failed) failed = true;
446
484
  }
485
+ } catch (error) {
486
+ fatalError = error;
487
+ failed = true;
488
+ throw error;
447
489
  } finally {
448
490
  await stopLocalServices(startedServices);
449
491
  }
450
492
 
451
- return failed;
493
+ return {
494
+ workerId: plan.workerId,
495
+ failed,
496
+ fatalError: fatalError ? formatError(fatalError) : null,
497
+ durationMs: Date.now() - startedAt,
498
+ suiteCount: plan.suites.length,
499
+ completedSuiteCount: suiteResults.length,
500
+ failedSuiteCount: suiteResults.filter((result) => result.failed).length,
501
+ suites: suiteResults,
502
+ };
452
503
  }
453
504
 
454
505
  async function prepareDatabases(runtimeConfigs) {
455
506
  for (const config of runtimeConfigs) {
456
- const db = config.testkit.database;
457
- if (!db) continue;
458
-
459
- requireNeonApiKey();
460
- fs.mkdirSync(config.stateDir, { recursive: true });
461
-
462
- await runScript("neon-up.sh", {
463
- NEON_PROJECT_ID: db.projectId,
464
- NEON_DB_NAME: db.dbName,
465
- NEON_BRANCH_NAME: db.branchName,
466
- NEON_RESET: db.reset === false ? "false" : "true",
467
- STATE_DIR: config.stateDir,
507
+ await prepareDatabaseRuntime(config, {
508
+ runMigrate: config.testkit.migrate
509
+ ? (databaseUrl) => runMigrate(config, databaseUrl)
510
+ : null,
511
+ runSeed: config.testkit.seed ? (databaseUrl) => runSeed(config, databaseUrl) : null,
468
512
  });
469
-
470
- fs.writeFileSync(path.join(config.stateDir, "neon_project_id"), db.projectId);
471
513
  }
472
514
  }
473
515
 
474
- async function runMigrations(runtimeConfigs) {
475
- for (const config of runtimeConfigs) {
476
- const migrate = config.testkit.migrate;
477
- if (!migrate) continue;
516
+ async function runMigrate(config, databaseUrl) {
517
+ const migrate = config.testkit.migrate;
518
+ if (!migrate) return;
478
519
 
479
- const env = buildExecutionEnv(config);
480
- const dbUrl = readDatabaseUrl(config.stateDir);
481
- if (dbUrl) env.DATABASE_URL = dbUrl;
520
+ const env = buildExecutionEnv(config);
521
+ if (databaseUrl) env.DATABASE_URL = databaseUrl;
482
522
 
483
- console.log(`\n── migrate:${config.workerLabel}:${config.name} ──`);
484
- await execaCommand(migrate.cmd, {
485
- cwd: resolveServiceCwd(config.productDir, migrate.cwd),
486
- env,
487
- stdio: "inherit",
488
- shell: true,
489
- });
490
- }
523
+ console.log(`\n── migrate:${config.workerLabel}:${config.name} ──`);
524
+ await execaCommand(migrate.cmd, {
525
+ cwd: resolveServiceCwd(config.productDir, migrate.cwd),
526
+ env,
527
+ stdio: "inherit",
528
+ shell: true,
529
+ });
491
530
  }
492
531
 
493
- async function runSeeds(runtimeConfigs) {
494
- for (const config of runtimeConfigs) {
495
- const seed = config.testkit.seed;
496
- if (!seed) continue;
532
+ async function runSeed(config, databaseUrl) {
533
+ const seed = config.testkit.seed;
534
+ if (!seed) return;
497
535
 
498
- const env = buildExecutionEnv(config);
499
- const dbUrl = readDatabaseUrl(config.stateDir);
500
- if (dbUrl) env.DATABASE_URL = dbUrl;
536
+ const env = buildExecutionEnv(config);
537
+ if (databaseUrl) env.DATABASE_URL = databaseUrl;
501
538
 
502
- console.log(`\n── seed:${config.workerLabel}:${config.name} ──`);
503
- await execaCommand(seed.cmd, {
504
- cwd: resolveServiceCwd(config.productDir, seed.cwd),
505
- env,
506
- stdio: "inherit",
507
- shell: true,
508
- });
509
- }
539
+ console.log(`\n── seed:${config.workerLabel}:${config.name} ──`);
540
+ await execaCommand(seed.cmd, {
541
+ cwd: resolveServiceCwd(config.productDir, seed.cwd),
542
+ env,
543
+ stdio: "inherit",
544
+ shell: true,
545
+ });
510
546
  }
511
547
 
512
548
  async function startLocalServices(runtimeConfigs) {
@@ -545,12 +581,15 @@ async function startLocalService(config) {
545
581
  const child = spawn(config.testkit.local.start, {
546
582
  cwd,
547
583
  env,
584
+ detached: true,
548
585
  shell: true,
549
586
  stdio: ["ignore", "pipe", "pipe"],
550
587
  });
551
588
 
552
- pipeOutput(child.stdout, `[${config.workerLabel}:${config.name}]`);
553
- pipeOutput(child.stderr, `[${config.workerLabel}:${config.name}]`);
589
+ const outputDrains = [
590
+ pipeOutput(child.stdout, `[${config.workerLabel}:${config.name}]`),
591
+ pipeOutput(child.stderr, `[${config.workerLabel}:${config.name}]`),
592
+ ];
554
593
 
555
594
  const readyTimeoutMs = config.testkit.local.readyTimeoutMs || DEFAULT_READY_TIMEOUT_MS;
556
595
 
@@ -562,11 +601,11 @@ async function startLocalService(config) {
562
601
  process: child,
563
602
  });
564
603
  } catch (error) {
565
- await stopChildProcess(child);
604
+ await stopChildProcess(child, outputDrains);
566
605
  throw error;
567
606
  }
568
607
 
569
- return { name: config.name, child };
608
+ return { name: config.name, child, outputDrains };
570
609
  }
571
610
 
572
611
  async function runSuite(targetConfig, suite) {
@@ -593,7 +632,9 @@ async function runHttpK6Suite(targetConfig, suite) {
593
632
  throw new Error(`Service "${targetConfig.name}" requires local.baseUrl for HTTP k6 suites`);
594
633
  }
595
634
 
635
+ const startedAt = Date.now();
596
636
  let failed = false;
637
+ const failedFiles = [];
597
638
  await runWithConcurrency(suite.files, suite.maxFileConcurrency, async (file) => {
598
639
  const absFile = path.join(targetConfig.productDir, file);
599
640
  console.log(`·· ${targetConfig.workerLabel}:${suite.name} → ${file}`);
@@ -605,10 +646,11 @@ async function runHttpK6Suite(targetConfig, suite) {
605
646
  });
606
647
  } catch {
607
648
  failed = true;
649
+ failedFiles.push(file);
608
650
  }
609
651
  });
610
652
 
611
- return { failed };
653
+ return buildSuiteResult(suite, failed, startedAt, failedFiles);
612
654
  }
613
655
 
614
656
  async function runDalSuite(targetConfig, suite) {
@@ -618,7 +660,9 @@ async function runDalSuite(targetConfig, suite) {
618
660
  }
619
661
 
620
662
  const k6Binary = resolveDalBinary();
663
+ const startedAt = Date.now();
621
664
  let failed = false;
665
+ const failedFiles = [];
622
666
  await runWithConcurrency(suite.files, suite.maxFileConcurrency, async (file) => {
623
667
  const absFile = path.join(targetConfig.productDir, file);
624
668
  console.log(`·· ${targetConfig.workerLabel}:${suite.name} → ${file}`);
@@ -630,10 +674,11 @@ async function runDalSuite(targetConfig, suite) {
630
674
  });
631
675
  } catch {
632
676
  failed = true;
677
+ failedFiles.push(file);
633
678
  }
634
679
  });
635
680
 
636
- return { failed };
681
+ return buildSuiteResult(suite, failed, startedAt, failedFiles);
637
682
  }
638
683
 
639
684
  async function runPlaywrightSuite(targetConfig, suite) {
@@ -648,6 +693,7 @@ async function runPlaywrightSuite(targetConfig, suite) {
648
693
  const files = suite.files.map((file) =>
649
694
  path.relative(cwd, path.join(targetConfig.productDir, file))
650
695
  );
696
+ const startedAt = Date.now();
651
697
 
652
698
  try {
653
699
  await execa("npx", ["playwright", "test", ...files], {
@@ -655,31 +701,37 @@ async function runPlaywrightSuite(targetConfig, suite) {
655
701
  env: buildPlaywrightEnv(targetConfig, local.baseUrl),
656
702
  stdio: "inherit",
657
703
  });
658
- return { failed: false };
704
+ return buildSuiteResult(suite, false, startedAt);
659
705
  } catch {
660
- return { failed: true };
706
+ return buildSuiteResult(suite, true, startedAt);
661
707
  }
662
708
  }
663
709
 
664
710
  async function stopLocalServices(started) {
665
711
  for (const service of [...started].reverse()) {
666
- await stopChildProcess(service.child);
712
+ await stopChildProcess(service.child, service.outputDrains);
667
713
  }
668
714
  }
669
715
 
670
- async function stopChildProcess(child) {
671
- if (!child || child.exitCode !== null) return;
716
+ async function stopChildProcess(child, outputDrains = []) {
717
+ if (!child) return;
718
+ if (child.exitCode !== null) {
719
+ await Promise.all(outputDrains);
720
+ return;
721
+ }
672
722
 
673
- child.kill("SIGTERM");
723
+ killChildProcess(child, "SIGTERM");
674
724
  const exited = await Promise.race([
675
725
  new Promise((resolve) => child.once("exit", () => resolve(true))),
676
726
  sleep(5_000).then(() => false),
677
727
  ]);
678
728
 
679
729
  if (!exited && child.exitCode === null) {
680
- child.kill("SIGKILL");
730
+ killChildProcess(child, "SIGKILL");
681
731
  await new Promise((resolve) => child.once("exit", resolve));
682
732
  }
733
+
734
+ await Promise.all(outputDrains);
683
735
  }
684
736
 
685
737
  async function waitForReady({ name, url, timeoutMs, process }) {
@@ -757,6 +809,124 @@ function resolvePlaywrightBrowsersPath(configuredPath) {
757
809
  return configuredPath;
758
810
  }
759
811
 
812
+ function buildSuiteResult(suite, failed, startedAt, failedFiles = []) {
813
+ return {
814
+ name: suite.name,
815
+ type: suite.type,
816
+ framework: suite.framework,
817
+ failed,
818
+ fileCount: suite.files.length,
819
+ failedFiles,
820
+ durationMs: Date.now() - startedAt,
821
+ };
822
+ }
823
+
824
+ function printRunSummary(results, durationMs) {
825
+ const totalServices = results.length;
826
+ const executedServices = results.filter((result) => !result.skipped);
827
+ const skippedServices = results.filter((result) => result.skipped);
828
+ const failedServices = executedServices.filter((result) => result.failed);
829
+ const passedServices = executedServices.filter((result) => !result.failed);
830
+ const totalSuites = executedServices.reduce((sum, result) => sum + result.suiteCount, 0);
831
+ const completedSuites = executedServices.reduce(
832
+ (sum, result) => sum + result.completedSuiteCount,
833
+ 0
834
+ );
835
+ const failedSuites = executedServices.reduce((sum, result) => sum + result.failedSuiteCount, 0);
836
+ const passedSuites = completedSuites - failedSuites;
837
+
838
+ console.log("\n══ Summary ══");
839
+ console.log(
840
+ [
841
+ `services ${passedServices.length}/${executedServices.length} passed`,
842
+ `suites ${passedSuites}/${totalSuites} passed`,
843
+ skippedServices.length > 0 ? `${skippedServices.length} skipped` : null,
844
+ `duration ${formatDuration(durationMs)}`,
845
+ ]
846
+ .filter(Boolean)
847
+ .join(" · ")
848
+ );
849
+
850
+ for (const result of results) {
851
+ const status = result.skipped ? "SKIP" : result.failed ? "FAIL" : "PASS";
852
+ const detail = result.skipped ? "no matching suites" : formatServiceSummary(result);
853
+ console.log(
854
+ `${status.padEnd(4)} ${result.name.padEnd(longestServiceName(results))} ${detail} · ${formatDuration(result.durationMs)}`
855
+ );
856
+
857
+ if (result.failed) {
858
+ const failedSuiteResults = result.workers.flatMap((worker) =>
859
+ worker.suites.filter((suite) => suite.failed)
860
+ );
861
+ for (const suite of failedSuiteResults) {
862
+ const fileDetail =
863
+ suite.failedFiles.length > 0 ? ` (${suite.failedFiles.join(", ")})` : "";
864
+ console.log(
865
+ ` - ${suite.type}:${suite.name} [${suite.framework}]${fileDetail} · ${formatDuration(suite.durationMs)}`
866
+ );
867
+ }
868
+ for (const error of result.errors) {
869
+ console.log(` - worker error: ${error}`);
870
+ }
871
+ }
872
+ }
873
+
874
+ if (failedServices.length > 0) {
875
+ console.log(`\nResult: FAILED (${failedServices.length}/${totalServices} services failed)`);
876
+ return;
877
+ }
878
+
879
+ console.log("\nResult: PASSED");
880
+ }
881
+
882
+ function longestServiceName(results) {
883
+ return results.reduce((max, result) => Math.max(max, result.name.length), 4);
884
+ }
885
+
886
+ function formatDuration(durationMs) {
887
+ const totalSeconds = Math.max(0, Math.round(durationMs / 1000));
888
+ const minutes = Math.floor(totalSeconds / 60);
889
+ const seconds = totalSeconds % 60;
890
+ if (minutes === 0) return `${seconds}s`;
891
+ return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
892
+ }
893
+
894
+ function formatError(error) {
895
+ if (error instanceof Error) return error.message;
896
+ return String(error);
897
+ }
898
+
899
+ function formatServiceSummary(result) {
900
+ const passedSuites = result.completedSuiteCount - result.failedSuiteCount;
901
+ const notRun = result.suiteCount - result.completedSuiteCount;
902
+ let detail = `${passedSuites}/${result.suiteCount} suites passed`;
903
+ if (notRun > 0) {
904
+ detail += `, ${notRun} not run`;
905
+ }
906
+ return detail;
907
+ }
908
+
909
+ function killChildProcess(child, signal) {
910
+ if (!child?.pid) return;
911
+
912
+ try {
913
+ process.kill(-child.pid, signal);
914
+ return;
915
+ } catch (error) {
916
+ if (error?.code !== "ESRCH") {
917
+ // Fall back to the direct child if process-group signalling is unavailable.
918
+ } else {
919
+ return;
920
+ }
921
+ }
922
+
923
+ try {
924
+ child.kill(signal);
925
+ } catch (error) {
926
+ if (error?.code !== "ESRCH") throw error;
927
+ }
928
+ }
929
+
760
930
  function readDatabaseUrl(stateDir) {
761
931
  return readStateValue(path.join(stateDir, "database_url"));
762
932
  }
@@ -780,21 +950,33 @@ function printStateDir(dir, indent) {
780
950
  }
781
951
 
782
952
  function pipeOutput(stream, prefix) {
783
- if (!stream) return;
953
+ if (!stream) return Promise.resolve();
784
954
 
785
955
  let pending = "";
786
- stream.on("data", (chunk) => {
787
- pending += chunk.toString();
788
- const lines = pending.split(/\r?\n/);
789
- pending = lines.pop() || "";
790
- for (const line of lines) {
791
- if (line.length > 0) console.log(`${prefix} ${line}`);
792
- }
793
- });
794
- stream.on("end", () => {
795
- if (pending.length > 0) {
796
- console.log(`${prefix} ${pending}`);
797
- }
956
+ return new Promise((resolve) => {
957
+ let settled = false;
958
+ const settle = () => {
959
+ if (settled) return;
960
+ settled = true;
961
+ resolve();
962
+ };
963
+
964
+ stream.on("data", (chunk) => {
965
+ pending += chunk.toString();
966
+ const lines = pending.split(/\r?\n/);
967
+ pending = lines.pop() || "";
968
+ for (const line of lines) {
969
+ if (line.length > 0) console.log(`${prefix} ${line}`);
970
+ }
971
+ });
972
+ stream.on("end", () => {
973
+ if (pending.length > 0) {
974
+ console.log(`${prefix} ${pending}`);
975
+ }
976
+ settle();
977
+ });
978
+ stream.on("close", settle);
979
+ stream.on("error", settle);
798
980
  });
799
981
  }
800
982
 
@@ -986,12 +1168,7 @@ function findRuntimeStateDirs(rootDir) {
986
1168
  const visit = (dir) => {
987
1169
  if (!fs.existsSync(dir)) return;
988
1170
  const entries = fs.readdirSync(dir, { withFileTypes: true });
989
- const hasRuntimeFiles = entries.some(
990
- (entry) =>
991
- entry.isFile() &&
992
- (entry.name === "neon_project_id" || entry.name === "neon_branch_id")
993
- );
994
- if (hasRuntimeFiles) {
1171
+ if (isDatabaseStateDir(dir)) {
995
1172
  found.push(dir);
996
1173
  }
997
1174
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "CLI for running manifest-defined local test suites across k6 and Playwright",
5
5
  "type": "module",
6
6
  "bin": {