@elench/testkit 0.1.22 → 0.1.23

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/README.md CHANGED
@@ -1,11 +1,10 @@
1
1
  # @elench/testkit
2
2
 
3
- CLI that reads `testkit.config.json` from a product repo, discovers `*.testkit.ts` files by convention, starts the required local services, provisions local Postgres isolation, and runs suites across `k6` and Playwright.
3
+ CLI that reads `testkit.config.json` from a product repo, discovers `*.testkit.ts` files by convention, starts the required local services, provisions local Postgres isolation, and runs HTTP, DAL, and Playwright suites.
4
4
 
5
5
  ## Prerequisites
6
6
 
7
- `@elench/testkit` ships its own `k6` binary and uses it for both HTTP and DAL suites by default.
8
- If you need to force a different binary, set `TESTKIT_K6_BIN` to an absolute or relative path.
7
+ `@elench/testkit` ships its own execution engine for HTTP and DAL suites. Consumers do not need to install or invoke any separate load-testing binary.
9
8
 
10
9
  Database isolation uses Docker-managed local Postgres containers. `testkit` creates and reuses template databases automatically, then clones per-worker databases from those templates for fast reruns. The default image is `pgvector/pgvector:pg16`.
11
10
 
@@ -24,7 +23,7 @@ npx @elench/testkit e2e
24
23
 
25
24
  # Filter by framework
26
25
  npx @elench/testkit --framework playwright
27
- npx @elench/testkit --framework k6
26
+ npx @elench/testkit --framework default
28
27
 
29
28
  # Parallelize with isolated worker stacks
30
29
  npx @elench/testkit --jobs 3
@@ -48,9 +47,9 @@ npx @elench/testkit status
48
47
  npx @elench/testkit destroy
49
48
  ```
50
49
 
51
- ## K6 Authoring
50
+ ## Authoring
52
51
 
53
- Consumer k6 tests can import the shared authoring API directly from the package:
52
+ Consumer tests can import the shared authoring API directly from the package:
54
53
 
55
54
  ```js
56
55
  import { defineHttpSuite } from "@elench/testkit";
@@ -76,15 +75,15 @@ const suite = defineHttpSuite({ auth: clerkSessionAuth }, ({ req, setupData }) =
76
75
  });
77
76
  ```
78
77
 
79
- `testkit` bundles these imports before invoking k6, so tests do not need
80
- generated `_testkit` files or direct package-manager path imports.
78
+ Low-level runtime primitives are also available without exposing the underlying engine:
81
79
 
82
- If you need to override the packaged `k6` binary for local environment reasons:
83
-
84
- ```bash
85
- TESTKIT_K6_BIN=/path/to/k6 npx @elench/testkit int
80
+ ```js
81
+ import { check, group, http } from "@elench/testkit/runtime";
86
82
  ```
87
83
 
84
+ `testkit` bundles these imports before execution, so tests do not need
85
+ generated `_testkit` files, direct package-manager path imports, or any separate engine installation.
86
+
88
87
  Legacy compatibility:
89
88
 
90
89
  - `testkit runtime install`
@@ -109,7 +108,7 @@ npx @elench/testkit --dir my-product api int -s health
109
108
  3. **Database** — provisions Docker-managed local Postgres when a service declares one
110
109
  4. **Seed** — runs optional product seed commands against the provisioned database
111
110
  5. **Runtime** — starts required local services, waits for readiness, and injects test env
112
- 6. **Execution** — schedules file-level execution tasks across a global worker pool, reuses warm dependency graphs when possible, bundles `k6` files before execution so package imports resolve cleanly, and batches Playwright files per worker
111
+ 6. **Execution** — schedules file-level execution tasks across a global worker pool, reuses warm dependency graphs when possible, bundles test files before execution so package imports resolve cleanly, and batches Playwright files per worker
113
112
  7. **Cleanup** — stops local processes and preserves `.testkit/` state for inspection or later destroy
114
113
 
115
114
  Each run ends with a compact summary that shows per-service pass/fail status, suite totals, failed suites, and total duration.
@@ -7,8 +7,7 @@ import { fileURLToPath } from "url";
7
7
 
8
8
  const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
9
9
  const ROOT_ENTRY = path.join(PACKAGE_ROOT, "lib", "index.mjs");
10
- const K6_ENTRY = path.join(PACKAGE_ROOT, "lib", "k6", "index.mjs");
11
- const K6_DIR = path.join(PACKAGE_ROOT, "lib", "k6");
10
+ const RUNTIME_ENTRY = path.join(PACKAGE_ROOT, "lib", "runtime", "index.mjs");
12
11
  const bundleCache = new Map();
13
12
 
14
13
  export async function bundleK6File({
@@ -82,14 +81,7 @@ function testkitPackageAliasPlugin() {
82
81
  function resolvePackageSubpath(specifier) {
83
82
  const subpath = specifier.slice("@elench/testkit".length);
84
83
  if (!subpath) return ROOT_ENTRY;
85
- if (subpath === "/k6") return K6_ENTRY;
86
- if (subpath.startsWith("/k6/")) {
87
- const rel = subpath.slice("/k6/".length);
88
- const candidate = path.join(K6_DIR, `${rel}.mjs`);
89
- if (fs.existsSync(candidate)) {
90
- return candidate;
91
- }
92
- }
84
+ if (subpath === "/runtime") return RUNTIME_ENTRY;
93
85
 
94
86
  throw new Error(`Unsupported @elench/testkit import "${specifier}" in ${os.platform()}`);
95
87
  }
@@ -12,8 +12,8 @@ afterEach(() => {
12
12
  }
13
13
  });
14
14
 
15
- describe("k6 bundler", () => {
16
- it("bundles root package imports for k6 execution", async () => {
15
+ describe("runtime bundler", () => {
16
+ it("bundles root and runtime package imports for execution", async () => {
17
17
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-bundle-"));
18
18
  cleanups.push(() => fs.rmSync(tmpDir, { force: true, recursive: true }));
19
19
 
@@ -21,8 +21,8 @@ describe("k6 bundler", () => {
21
21
  fs.writeFileSync(
22
22
  sourceFile,
23
23
  [
24
- 'import { defineHttpSuite, json } from "@elench/testkit";',
25
- 'import { check } from "k6";',
24
+ 'import { defineHttpSuite } from "@elench/testkit";',
25
+ 'import { check, json } from "@elench/testkit/runtime";',
26
26
  "const suite = defineHttpSuite(({ rawReq }) => {",
27
27
  ' const res = rawReq("GET", "/health");',
28
28
  " check(json(res), {",
@@ -44,10 +44,11 @@ describe("k6 bundler", () => {
44
44
 
45
45
  const bundled = fs.readFileSync(bundledFile, "utf8");
46
46
  expect(bundled).toContain("defineHttpSuite");
47
- expect(bundled).toContain('import { check } from "k6"');
47
+ expect(bundled).toContain('import { check');
48
+ expect(bundled).toContain('from "k6"');
48
49
  });
49
50
 
50
- it("bundles subpath package imports for DAL execution", async () => {
51
+ it("bundles DAL execution through the public package surface", async () => {
51
52
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-bundle-"));
52
53
  cleanups.push(() => fs.rmSync(tmpDir, { force: true, recursive: true }));
53
54
 
@@ -55,7 +56,7 @@ describe("k6 bundler", () => {
55
56
  fs.writeFileSync(
56
57
  sourceFile,
57
58
  [
58
- 'import { defineDalSuite } from "@elench/testkit/k6";',
59
+ 'import { defineDalSuite } from "@elench/testkit";',
59
60
  "const suite = defineDalSuite(({ db }) => {",
60
61
  ' db.query("SELECT 1");',
61
62
  "});",
package/lib/cli/args.mjs CHANGED
@@ -22,11 +22,12 @@ export function resolveCliSelection({ first, second, serviceNames }) {
22
22
  }
23
23
 
24
24
  export function validateFrameworkOption(value) {
25
- if (!["all", "k6", "playwright"].includes(value)) {
25
+ if (!["all", "default", "playwright"].includes(value)) {
26
26
  throw new Error(
27
- `Unknown framework "${value}". Expected one of: all, k6, playwright.`
27
+ `Unknown framework "${value}". Expected one of: all, default, playwright.`
28
28
  );
29
29
  }
30
+ return value === "default" ? "k6" : value;
30
31
  }
31
32
 
32
33
  export function parseJobsOption(value) {
@@ -44,7 +44,8 @@ describe("cli-args", () => {
44
44
  });
45
45
 
46
46
  it("validates framework names", () => {
47
- expect(() => validateFrameworkOption("playwright")).not.toThrow();
47
+ expect(validateFrameworkOption("playwright")).toBe("playwright");
48
+ expect(validateFrameworkOption("default")).toBe("k6");
48
49
  expect(() => validateFrameworkOption("jest")).toThrow("Unknown framework");
49
50
  });
50
51
 
package/lib/cli/index.mjs CHANGED
@@ -8,7 +8,6 @@ import {
8
8
  validateFrameworkOption,
9
9
  } from "./args.mjs";
10
10
  import * as runner from "../runner/index.mjs";
11
- import * as runtime from "../runtime/index.mjs";
12
11
 
13
12
  export function run() {
14
13
  const cli = cac("testkit");
@@ -18,11 +17,13 @@ export function run() {
18
17
  .option("--dir <path>", "Explicit product directory")
19
18
  .option("--path <path>", "Target runtime path relative to the product directory")
20
19
  .option("--strict", "Exit non-zero when runtime status is missing or drifted")
21
- .action((action, options) => {
20
+ .action(async (action, options) => {
22
21
  if (!["install", "status", "update"].includes(action)) {
23
22
  throw new Error('Unknown runtime action. Expected one of: install, status, update.');
24
23
  }
25
24
 
25
+ const runtime = await import("../runtime-manager/index.mjs");
26
+
26
27
  if (action === "status") {
27
28
  const status = runtime.getRuntimeStatus(options);
28
29
  console.log(runtime.formatRuntimeStatus(status));
@@ -47,7 +48,7 @@ export function run() {
47
48
  default: "1",
48
49
  })
49
50
  .option("--shard <i/n>", "Run only shard i of n at suite granularity")
50
- .option("--framework <name>", "Filter by framework (k6, playwright, all)", {
51
+ .option("--framework <name>", "Filter by framework (default, playwright, all)", {
51
52
  default: "all",
52
53
  })
53
54
  .option("--write-status", "Write a deterministic testkit.status.json snapshot")
@@ -91,7 +92,7 @@ export function run() {
91
92
  return;
92
93
  }
93
94
 
94
- validateFrameworkOption(options.framework);
95
+ const framework = validateFrameworkOption(options.framework);
95
96
 
96
97
  const jobs = parseJobsOption(options.jobs);
97
98
  const shard = parseShardOption(options.shard);
@@ -105,6 +106,7 @@ export function run() {
105
106
  suiteNames,
106
107
  {
107
108
  ...options,
109
+ framework,
108
110
  fileNames,
109
111
  jobs,
110
112
  shard,
package/lib/index.mjs CHANGED
@@ -1 +1,13 @@
1
- export * from "./k6/index.mjs";
1
+ export {
2
+ defineDalSuite,
3
+ } from "./runtime-src/k6/dal-suite.js";
4
+ export {
5
+ defineHttpSuite,
6
+ } from "./runtime-src/k6/suite.js";
7
+
8
+ export function createAuthAdapter({ setup, headers } = {}) {
9
+ return {
10
+ setup,
11
+ headers,
12
+ };
13
+ }
@@ -535,11 +535,11 @@ async function startLocalService(config) {
535
535
  async function runHttpK6Batch(targetConfig, batch) {
536
536
  const baseUrl = targetConfig.testkit.local?.baseUrl;
537
537
  if (!baseUrl) {
538
- throw new Error(`Service "${targetConfig.name}" requires local.baseUrl for HTTP k6 suites`);
538
+ throw new Error(`Service "${targetConfig.name}" requires local.baseUrl for HTTP suites`);
539
539
  }
540
540
 
541
541
  console.log(
542
- `\n── ${targetConfig.workerLabel} ${batch.type}:${batch.tasks[0].suiteName} (${batch.framework}, ${batch.tasks.length} file${batch.tasks.length === 1 ? "" : "s"}) ──`
542
+ `\n── ${targetConfig.workerLabel} ${batch.type}:${batch.tasks[0].suiteName}${formatBatchDescriptor(batch)} ──`
543
543
  );
544
544
 
545
545
  return Promise.all(
@@ -586,7 +586,7 @@ async function runDalBatch(targetConfig, batch) {
586
586
  }
587
587
 
588
588
  console.log(
589
- `\n── ${targetConfig.workerLabel} ${batch.type}:${batch.tasks[0].suiteName} (${batch.framework}, ${batch.tasks.length} file${batch.tasks.length === 1 ? "" : "s"}) ──`
589
+ `\n── ${targetConfig.workerLabel} ${batch.type}:${batch.tasks[0].suiteName}${formatBatchDescriptor(batch)} ──`
590
590
  );
591
591
 
592
592
  return Promise.all(
@@ -866,7 +866,7 @@ function printRunSummary(results, durationMs) {
866
866
  const fileDetail =
867
867
  suite.failedFiles.length > 0 ? ` (${suite.failedFiles.join(", ")})` : "";
868
868
  console.log(
869
- ` - ${suite.type}:${suite.name} [${suite.framework}]${fileDetail} · ${formatDuration(suite.durationMs)}`
869
+ ` - ${suite.type}:${suite.name}${formatSuiteFramework(suite.framework)}${fileDetail} · ${formatDuration(suite.durationMs)}`
870
870
  );
871
871
  if (suite.error) {
872
872
  console.log(` ${suite.error}`);
@@ -911,6 +911,22 @@ function longestServiceName(results) {
911
911
  return longestServiceNameModel(results);
912
912
  }
913
913
 
914
+ function formatBatchDescriptor(batch) {
915
+ const fileLabel = `${batch.tasks.length} file${batch.tasks.length === 1 ? "" : "s"}`;
916
+ const frameworkLabel = formatFrameworkLabel(batch.framework);
917
+ return frameworkLabel ? ` (${frameworkLabel}, ${fileLabel})` : ` (${fileLabel})`;
918
+ }
919
+
920
+ function formatFrameworkLabel(framework) {
921
+ if (!framework || framework === "k6") return "";
922
+ return framework;
923
+ }
924
+
925
+ function formatSuiteFramework(framework) {
926
+ const label = formatFrameworkLabel(framework);
927
+ return label ? ` [${label}]` : "";
928
+ }
929
+
914
930
  function buildRunArtifact({
915
931
  productDir,
916
932
  results,
@@ -177,7 +177,7 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
177
177
  suites: suites.map((suite) => ({
178
178
  name: suite.name,
179
179
  type: suite.type,
180
- framework: suite.framework,
180
+ framework: formatFrameworkForArtifact(suite.framework),
181
181
  failed: suite.failedFiles.length > 0,
182
182
  fileCount: suite.fileCount,
183
183
  failedFiles: suite.failedFiles,
@@ -212,7 +212,6 @@ export function buildStatusArtifact({
212
212
  tests.push({
213
213
  service: result.name,
214
214
  type: suite.type,
215
- framework: suite.framework,
216
215
  path: file.path,
217
216
  status: file.status,
218
217
  });
@@ -301,7 +300,7 @@ export function buildRunArtifact({
301
300
  suiteType,
302
301
  suiteNames,
303
302
  fileNames,
304
- framework,
303
+ framework: formatFrameworkForArtifact(framework),
305
304
  shard,
306
305
  serviceFilter,
307
306
  testkitVersion: metadata.testkitVersion,
@@ -360,8 +359,8 @@ export function formatServiceSummary(result) {
360
359
  }
361
360
 
362
361
  export function formatError(error) {
363
- if (error instanceof Error) return error.message;
364
- return String(error);
362
+ if (error instanceof Error) return sanitizeErrorMessage(error.message);
363
+ return sanitizeErrorMessage(String(error));
365
364
  }
366
365
 
367
366
  export function longestServiceName(results) {
@@ -371,3 +370,15 @@ export function longestServiceName(results) {
371
370
  function normalizePathSeparators(filePath) {
372
371
  return filePath.split(path.sep).join("/");
373
372
  }
373
+
374
+ function formatFrameworkForArtifact(framework) {
375
+ if (framework === "k6") return "default";
376
+ return framework;
377
+ }
378
+
379
+ function sanitizeErrorMessage(message) {
380
+ return message
381
+ .replace(/Command failed with exit code (\d+): .*?[\\/]vendor[\\/]k6 run\b/g, "Default runtime failed with exit code $1:")
382
+ .replace(/Command failed with exit code (\d+): k6 run\b/g, "Default runtime failed with exit code $1:")
383
+ .replace(/[\\/]vendor[\\/]k6\b/g, "default-runtime");
384
+ }
@@ -65,6 +65,7 @@ describe("runner-results", () => {
65
65
  expect(result.failed).toBe(true);
66
66
  expect(result.failedSuiteCount).toBe(1);
67
67
  expect(result.errors).toEqual(["worker failed", "graph failed"]);
68
+ expect(result.suites[0].framework).toBe("default");
68
69
  expect(result.suites[0].files).toEqual([
69
70
  {
70
71
  path: "tests/health.js",
@@ -169,7 +170,7 @@ describe("runner-results", () => {
169
170
  suiteType: "int",
170
171
  suiteNames: ["health"],
171
172
  fileNames: ["tests/api/integration/b.int.testkit.ts"],
172
- framework: "k6",
173
+ framework: "default",
173
174
  shard: null,
174
175
  serviceFilter: "api",
175
176
  metadata: {
@@ -210,14 +211,12 @@ describe("runner-results", () => {
210
211
  {
211
212
  service: "api",
212
213
  type: "integration",
213
- framework: "k6",
214
214
  path: "tests/api/integration/a.int.testkit.ts",
215
215
  status: "passed",
216
216
  },
217
217
  {
218
218
  service: "api",
219
219
  type: "integration",
220
- framework: "k6",
221
220
  path: "tests/api/integration/b.int.testkit.ts",
222
221
  status: "failed",
223
222
  },
@@ -1,190 +1,31 @@
1
- import crypto from "crypto";
2
- import fs from "fs";
3
- import path from "path";
4
- import { fileURLToPath } from "url";
5
-
6
- const TESTKIT_CONFIG = "testkit.config.json";
7
- const DEFAULT_RUNTIME_DIR = path.join("tests", "_testkit");
8
- const METADATA_FILE = ".runtime-manifest.json";
9
- const RUNTIME_FORMAT = 1;
10
-
11
- export function installRuntime(options = {}) {
12
- const productDir = resolveProductDir(process.cwd(), options.dir);
13
- const runtimeDir = resolveRuntimeDir(productDir, options.path);
14
- const sourceFiles = readBundledRuntimeFiles();
15
-
16
- fs.mkdirSync(runtimeDir, { recursive: true });
17
-
18
- for (const file of sourceFiles) {
19
- const targetPath = path.join(runtimeDir, file.path);
20
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
21
- fs.writeFileSync(targetPath, file.content);
22
- }
23
-
24
- const metadata = {
25
- format: RUNTIME_FORMAT,
26
- package: "@elench/testkit",
27
- version: readPackageVersion(),
28
- files: sourceFiles.map((file) => ({
29
- path: file.path,
30
- sha256: hashContent(file.content),
31
- })),
32
- };
33
- fs.writeFileSync(
34
- path.join(runtimeDir, METADATA_FILE),
35
- `${JSON.stringify(metadata, null, 2)}\n`
36
- );
37
-
38
- return {
39
- productDir,
40
- runtimeDir,
41
- relativeRuntimeDir: relativeToProduct(productDir, runtimeDir),
42
- files: metadata.files,
43
- };
44
- }
45
-
46
- export function getRuntimeStatus(options = {}) {
47
- const productDir = resolveProductDir(process.cwd(), options.dir);
48
- const runtimeDir = resolveRuntimeDir(productDir, options.path);
49
- const sourceFiles = readBundledRuntimeFiles();
50
- const metadataPath = path.join(runtimeDir, METADATA_FILE);
51
-
52
- if (!fs.existsSync(runtimeDir) || !fs.existsSync(metadataPath)) {
53
- return {
54
- status: "missing",
55
- productDir,
56
- runtimeDir,
57
- relativeRuntimeDir: relativeToProduct(productDir, runtimeDir),
58
- missingFiles: sourceFiles.map((file) => file.path),
59
- driftedFiles: [],
60
- };
61
- }
62
-
63
- const missingFiles = [];
64
- const driftedFiles = [];
65
-
66
- for (const file of sourceFiles) {
67
- const targetPath = path.join(runtimeDir, file.path);
68
- if (!fs.existsSync(targetPath)) {
69
- missingFiles.push(file.path);
70
- continue;
71
- }
72
-
73
- const installed = fs.readFileSync(targetPath, "utf8");
74
- if (installed !== file.content) {
75
- driftedFiles.push(file.path);
76
- }
77
- }
78
-
79
- const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf8"));
80
- const versionMatches = metadata.version === readPackageVersion();
81
-
82
- return {
83
- status:
84
- missingFiles.length === 0 && driftedFiles.length === 0 && versionMatches
85
- ? "installed"
86
- : "drifted",
87
- productDir,
88
- runtimeDir,
89
- relativeRuntimeDir: relativeToProduct(productDir, runtimeDir),
90
- versionMatches,
91
- missingFiles,
92
- driftedFiles,
93
- };
94
- }
95
-
96
- export function formatRuntimeStatus(result) {
97
- if (result.status === "missing") {
98
- return `Runtime not installed at ${result.relativeRuntimeDir}`;
99
- }
100
-
101
- if (result.status === "installed") {
102
- return `Runtime at ${result.relativeRuntimeDir} is up to date`;
103
- }
104
-
105
- const problems = [];
106
- if (result.missingFiles.length > 0) {
107
- problems.push(`missing: ${result.missingFiles.join(", ")}`);
108
- }
109
- if (result.driftedFiles.length > 0) {
110
- problems.push(`drifted: ${result.driftedFiles.join(", ")}`);
111
- }
112
- if (result.versionMatches === false) {
113
- problems.push("version drift");
114
- }
115
-
116
- return `Runtime at ${result.relativeRuntimeDir} is drifted (${problems.join("; ")})`;
117
- }
118
-
119
- function resolveProductDir(cwd, explicitDir) {
120
- const dir = explicitDir ? path.resolve(cwd, explicitDir) : cwd;
121
- ensureProductFiles(dir);
122
- return dir;
123
- }
124
-
125
- function ensureProductFiles(dir) {
126
- const missing = [TESTKIT_CONFIG].filter(
127
- (file) => !fs.existsSync(path.join(dir, file))
128
- );
129
-
130
- if (missing.length > 0) {
131
- throw new Error(
132
- `Expected ${missing.join(" and ")} in ${dir}. Either cd into a product directory or use --dir.`
133
- );
134
- }
135
- }
136
-
137
- function resolveRuntimeDir(productDir, explicitPath) {
138
- return path.resolve(productDir, explicitPath || DEFAULT_RUNTIME_DIR);
139
- }
140
-
141
- function relativeToProduct(productDir, targetPath) {
142
- return path.relative(productDir, targetPath) || ".";
143
- }
144
-
145
- function readBundledRuntimeFiles() {
146
- const sourceDir = path.resolve(
147
- path.dirname(fileURLToPath(import.meta.url)),
148
- "..",
149
- "runtime-src"
150
- );
151
-
152
- return walkRuntimeFiles(sourceDir);
153
- }
154
-
155
- function walkRuntimeFiles(rootDir, relativeDir = "") {
156
- const entries = fs.readdirSync(path.join(rootDir, relativeDir), {
157
- withFileTypes: true,
158
- });
159
- const files = [];
160
-
161
- for (const entry of entries) {
162
- const nextRelative = path.join(relativeDir, entry.name);
163
- if (entry.isDirectory()) {
164
- files.push(...walkRuntimeFiles(rootDir, nextRelative));
165
- continue;
166
- }
167
-
168
- const absolute = path.join(rootDir, nextRelative);
169
- files.push({
170
- path: nextRelative.split(path.sep).join("/"),
171
- content: fs.readFileSync(absolute, "utf8"),
172
- });
173
- }
174
-
175
- return files.sort((left, right) => left.path.localeCompare(right.path));
176
- }
177
-
178
- function readPackageVersion() {
179
- const packagePath = path.resolve(
180
- path.dirname(fileURLToPath(import.meta.url)),
181
- "..",
182
- "..",
183
- "package.json"
184
- );
185
- return JSON.parse(fs.readFileSync(packagePath, "utf8")).version;
186
- }
187
-
188
- function hashContent(content) {
189
- return crypto.createHash("sha256").update(content).digest("hex");
190
- }
1
+ import rawHttp from "k6/http";
2
+ import { check, fail, group, sleep } from "k6";
3
+
4
+ export { check, fail, group, sleep };
5
+ export const http = rawHttp;
6
+
7
+ export function file(data, filename, contentType) {
8
+ return rawHttp.file(data, filename, contentType);
9
+ }
10
+
11
+ export {
12
+ allMatch,
13
+ contains,
14
+ defaultOptions,
15
+ isSorted,
16
+ json,
17
+ singleIterationOptions,
18
+ } from "../runtime-src/k6/checks.js";
19
+ export {
20
+ createDalContext,
21
+ openDb,
22
+ truncate,
23
+ } from "../runtime-src/k6/dal.js";
24
+ export {
25
+ createHttpClient,
26
+ defaultOptions as httpDefaultOptions,
27
+ getEnv,
28
+ makeGetWithHeaders,
29
+ makeRawReq,
30
+ makeReq,
31
+ } from "../runtime-src/k6/http.js";
@@ -0,0 +1,190 @@
1
+ import crypto from "crypto";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const TESTKIT_CONFIG = "testkit.config.json";
7
+ const DEFAULT_RUNTIME_DIR = path.join("tests", "_testkit");
8
+ const METADATA_FILE = ".runtime-manifest.json";
9
+ const RUNTIME_FORMAT = 1;
10
+
11
+ export function installRuntime(options = {}) {
12
+ const productDir = resolveProductDir(process.cwd(), options.dir);
13
+ const runtimeDir = resolveRuntimeDir(productDir, options.path);
14
+ const sourceFiles = readBundledRuntimeFiles();
15
+
16
+ fs.mkdirSync(runtimeDir, { recursive: true });
17
+
18
+ for (const file of sourceFiles) {
19
+ const targetPath = path.join(runtimeDir, file.path);
20
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
21
+ fs.writeFileSync(targetPath, file.content);
22
+ }
23
+
24
+ const metadata = {
25
+ format: RUNTIME_FORMAT,
26
+ package: "@elench/testkit",
27
+ version: readPackageVersion(),
28
+ files: sourceFiles.map((file) => ({
29
+ path: file.path,
30
+ sha256: hashContent(file.content),
31
+ })),
32
+ };
33
+ fs.writeFileSync(
34
+ path.join(runtimeDir, METADATA_FILE),
35
+ `${JSON.stringify(metadata, null, 2)}\n`
36
+ );
37
+
38
+ return {
39
+ productDir,
40
+ runtimeDir,
41
+ relativeRuntimeDir: relativeToProduct(productDir, runtimeDir),
42
+ files: metadata.files,
43
+ };
44
+ }
45
+
46
+ export function getRuntimeStatus(options = {}) {
47
+ const productDir = resolveProductDir(process.cwd(), options.dir);
48
+ const runtimeDir = resolveRuntimeDir(productDir, options.path);
49
+ const sourceFiles = readBundledRuntimeFiles();
50
+ const metadataPath = path.join(runtimeDir, METADATA_FILE);
51
+
52
+ if (!fs.existsSync(runtimeDir) || !fs.existsSync(metadataPath)) {
53
+ return {
54
+ status: "missing",
55
+ productDir,
56
+ runtimeDir,
57
+ relativeRuntimeDir: relativeToProduct(productDir, runtimeDir),
58
+ missingFiles: sourceFiles.map((file) => file.path),
59
+ driftedFiles: [],
60
+ };
61
+ }
62
+
63
+ const missingFiles = [];
64
+ const driftedFiles = [];
65
+
66
+ for (const file of sourceFiles) {
67
+ const targetPath = path.join(runtimeDir, file.path);
68
+ if (!fs.existsSync(targetPath)) {
69
+ missingFiles.push(file.path);
70
+ continue;
71
+ }
72
+
73
+ const installed = fs.readFileSync(targetPath, "utf8");
74
+ if (installed !== file.content) {
75
+ driftedFiles.push(file.path);
76
+ }
77
+ }
78
+
79
+ const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf8"));
80
+ const versionMatches = metadata.version === readPackageVersion();
81
+
82
+ return {
83
+ status:
84
+ missingFiles.length === 0 && driftedFiles.length === 0 && versionMatches
85
+ ? "installed"
86
+ : "drifted",
87
+ productDir,
88
+ runtimeDir,
89
+ relativeRuntimeDir: relativeToProduct(productDir, runtimeDir),
90
+ versionMatches,
91
+ missingFiles,
92
+ driftedFiles,
93
+ };
94
+ }
95
+
96
+ export function formatRuntimeStatus(result) {
97
+ if (result.status === "missing") {
98
+ return `Runtime not installed at ${result.relativeRuntimeDir}`;
99
+ }
100
+
101
+ if (result.status === "installed") {
102
+ return `Runtime at ${result.relativeRuntimeDir} is up to date`;
103
+ }
104
+
105
+ const problems = [];
106
+ if (result.missingFiles.length > 0) {
107
+ problems.push(`missing: ${result.missingFiles.join(", ")}`);
108
+ }
109
+ if (result.driftedFiles.length > 0) {
110
+ problems.push(`drifted: ${result.driftedFiles.join(", ")}`);
111
+ }
112
+ if (result.versionMatches === false) {
113
+ problems.push("version drift");
114
+ }
115
+
116
+ return `Runtime at ${result.relativeRuntimeDir} is drifted (${problems.join("; ")})`;
117
+ }
118
+
119
+ function resolveProductDir(cwd, explicitDir) {
120
+ const dir = explicitDir ? path.resolve(cwd, explicitDir) : cwd;
121
+ ensureProductFiles(dir);
122
+ return dir;
123
+ }
124
+
125
+ function ensureProductFiles(dir) {
126
+ const missing = [TESTKIT_CONFIG].filter(
127
+ (file) => !fs.existsSync(path.join(dir, file))
128
+ );
129
+
130
+ if (missing.length > 0) {
131
+ throw new Error(
132
+ `Expected ${missing.join(" and ")} in ${dir}. Either cd into a product directory or use --dir.`
133
+ );
134
+ }
135
+ }
136
+
137
+ function resolveRuntimeDir(productDir, explicitPath) {
138
+ return path.resolve(productDir, explicitPath || DEFAULT_RUNTIME_DIR);
139
+ }
140
+
141
+ function relativeToProduct(productDir, targetPath) {
142
+ return path.relative(productDir, targetPath) || ".";
143
+ }
144
+
145
+ function readBundledRuntimeFiles() {
146
+ const sourceDir = path.resolve(
147
+ path.dirname(fileURLToPath(import.meta.url)),
148
+ "..",
149
+ "runtime-src"
150
+ );
151
+
152
+ return walkRuntimeFiles(sourceDir);
153
+ }
154
+
155
+ function walkRuntimeFiles(rootDir, relativeDir = "") {
156
+ const entries = fs.readdirSync(path.join(rootDir, relativeDir), {
157
+ withFileTypes: true,
158
+ });
159
+ const files = [];
160
+
161
+ for (const entry of entries) {
162
+ const nextRelative = path.join(relativeDir, entry.name);
163
+ if (entry.isDirectory()) {
164
+ files.push(...walkRuntimeFiles(rootDir, nextRelative));
165
+ continue;
166
+ }
167
+
168
+ const absolute = path.join(rootDir, nextRelative);
169
+ files.push({
170
+ path: nextRelative.split(path.sep).join("/"),
171
+ content: fs.readFileSync(absolute, "utf8"),
172
+ });
173
+ }
174
+
175
+ return files.sort((left, right) => left.path.localeCompare(right.path));
176
+ }
177
+
178
+ function readPackageVersion() {
179
+ const packagePath = path.resolve(
180
+ path.dirname(fileURLToPath(import.meta.url)),
181
+ "..",
182
+ "..",
183
+ "package.json"
184
+ );
185
+ return JSON.parse(fs.readFileSync(packagePath, "utf8")).version;
186
+ }
187
+
188
+ function hashContent(content) {
189
+ return crypto.createHash("sha256").update(content).digest("hex");
190
+ }
package/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "@elench/testkit",
3
- "version": "0.1.22",
4
- "description": "CLI for discovering and running local test suites across k6 and Playwright",
3
+ "version": "0.1.23",
4
+ "description": "CLI for discovering and running local HTTP, DAL, and Playwright test suites",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./lib/index.mjs",
8
- "./k6": "./lib/k6/index.mjs",
9
- "./k6/*": "./lib/k6/*.mjs",
8
+ "./runtime": "./lib/runtime/index.mjs",
10
9
  "./package.json": "./package.json"
11
10
  },
12
11
  "bin": {
package/lib/k6/checks.mjs DELETED
@@ -1 +0,0 @@
1
- export * from "../runtime-src/k6/checks.js";
@@ -1 +0,0 @@
1
- export * from "../runtime-src/k6/dal-suite.js";
package/lib/k6/dal.mjs DELETED
@@ -1 +0,0 @@
1
- export * from "../runtime-src/k6/dal.js";
package/lib/k6/http.mjs DELETED
@@ -1 +0,0 @@
1
- export * from "../runtime-src/k6/http.js";
package/lib/k6/index.mjs DELETED
@@ -1,30 +0,0 @@
1
- export {
2
- allMatch,
3
- contains,
4
- defaultOptions,
5
- isSorted,
6
- json,
7
- singleIterationOptions,
8
- } from "../runtime-src/k6/checks.js";
9
- export {
10
- createDalContext,
11
- openDb,
12
- truncate,
13
- } from "../runtime-src/k6/dal.js";
14
- export { defineDalSuite } from "../runtime-src/k6/dal-suite.js";
15
- export {
16
- createHttpClient,
17
- defaultOptions as httpDefaultOptions,
18
- getEnv,
19
- makeGetWithHeaders,
20
- makeRawReq,
21
- makeReq,
22
- } from "../runtime-src/k6/http.js";
23
- export { defineHttpSuite } from "../runtime-src/k6/suite.js";
24
-
25
- export function createAuthAdapter({ setup, headers } = {}) {
26
- return {
27
- setup,
28
- headers,
29
- };
30
- }
package/lib/k6/suite.mjs DELETED
@@ -1 +0,0 @@
1
- export * from "../runtime-src/k6/suite.js";