@elench/testkit 0.1.10 → 0.1.12

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,44 +1,43 @@
1
1
  # @elench/testkit
2
2
 
3
- CLI that reads `testkit.manifest.json` from a product repo, spins up ephemeral infrastructure (Neon DB branch + Fly machine) per service, runs k6 tests, and tears down.
3
+ CLI that reads `runner.manifest.json` plus `testkit.config.json` from a product repo, starts the required local services, provisions Neon branches when configured, and runs manifest-defined suites across `k6` and Playwright.
4
4
 
5
5
  ## Prerequisites
6
6
 
7
7
  ```bash
8
8
  sudo snap install k6
9
9
  sudo apt-get install -y jq
10
- curl -L https://fly.io/install.sh | sh
11
- fly auth login
12
10
  ```
13
11
 
14
- ## Setup
15
-
16
- Add platform secrets to each product's `.env`:
17
-
18
- ```bash
19
- NEON_API_KEY='...'
20
- FLY_API_TOKEN='...'
21
- ```
22
-
23
- Product secrets (`CLERK_SECRET_KEY`, etc.) are also loaded from the same `.env`.
12
+ For DAL suites, `@elench/testkit` ships its own `k6` SQL binary. For suites using a Neon branch, set `NEON_API_KEY` in the product `.env`, shell, or `.envrc`.
24
13
 
25
14
  ## Usage
26
15
 
27
16
  ```bash
28
17
  cd bourne
29
18
 
30
- # Run all suites
19
+ # Run every testkit-managed suite
31
20
  npx @elench/testkit
32
21
 
33
- # Specific type / suite
34
- npx @elench/testkit int -s health
22
+ # Filter by type
23
+ npx @elench/testkit int
24
+ npx @elench/testkit dal
35
25
  npx @elench/testkit e2e
36
26
 
37
- # Specific service (multi-service products)
38
- npx @elench/testkit avocado_api int -s health
27
+ # Filter by framework
28
+ npx @elench/testkit --framework playwright
29
+ npx @elench/testkit --framework k6
39
30
 
40
- # Build from source first
41
- npx @elench/testkit int --build
31
+ # Parallelize with isolated worker stacks
32
+ npx @elench/testkit --jobs 3
33
+
34
+ # Run a deterministic shard
35
+ npx @elench/testkit --shard 1/3
36
+ npx @elench/testkit --jobs 2 --shard 2/3
37
+
38
+ # Specific service / suite
39
+ npx @elench/testkit frontend e2e -s auth
40
+ npx @elench/testkit bourne int -s health
42
41
 
43
42
  # Lifecycle
44
43
  npx @elench/testkit status
@@ -47,17 +46,50 @@ npx @elench/testkit destroy
47
46
 
48
47
  ## How it works
49
48
 
50
- 1. **Config** — reads `testkit.manifest.json` for per-service infra config + test suites
51
- 2. **Neon** — discovers or creates a `<service>-test` branch, truncates tables between runs
52
- 3. **Fly** — discovers or creates a machine on the service's test app, updates env vars
53
- 4. **k6** — runs matched test files with `BASE_URL` and `MACHINE_ID` injected
54
- 5. **DAL** — DAL tests use a bundled k6-sql binary (`vendor/k6`) no external binary needed
55
- 6. **Cleanup** — stops the Fly machine (preserved for next run)
49
+ 1. **Discovery** — reads `runner.manifest.json` for services, suites, files, and frameworks
50
+ 2. **Config** — reads `testkit.config.json` for local runtime, migration, dependency, and database settings
51
+ 3. **Database** — provisions a Neon branch when a service declares one
52
+ 4. **Seed** — runs optional product seed commands against the provisioned database
53
+ 5. **Runtime** — starts required local services, waits for readiness, and injects test env
54
+ 6. **Execution** — distributes suites across isolated worker stacks, runs `k6` suites file-by-file, and runs Playwright suites suite-by-suite
55
+ 7. **Cleanup** — stops local processes and preserves `.testkit/` state for inspection or later destroy
56
+
57
+ ## File roles
58
+
59
+ - `runner.manifest.json`: canonical test inventory
60
+ - `testkit.config.json`: local execution and provisioning config
56
61
 
57
- Multi-service products run all services in parallel. Each service gets its own Neon branch and Fly machine.
62
+ ## Parallel execution
63
+
64
+ `@elench/testkit` can run suites in parallel with `--jobs <n>`.
65
+
66
+ Each worker gets its own:
67
+ - Neon branch
68
+ - `.testkit` state subtree
69
+ - local service ports
70
+
71
+ This keeps suites isolated while still reusing one stack per worker across multiple assigned suites.
72
+
73
+ Use `--shard <i/n>` to split the selected suite set deterministically before worker scheduling.
74
+
75
+ ## Suite metadata
76
+
77
+ `runner.manifest.json` remains the source of truth for suites. Optional per-suite `testkit` metadata can tune execution:
78
+
79
+ ```json
80
+ {
81
+ "name": "health",
82
+ "files": ["tests/example.js"],
83
+ "testkit": {
84
+ "maxFileConcurrency": 2,
85
+ "weight": 3
86
+ }
87
+ }
88
+ ```
58
89
 
59
- State is persisted in `.testkit/` (or `.testkit/<service>/` for multi-service) so subsequent runs reuse existing infrastructure.
90
+ - `maxFileConcurrency`: k6-only opt-in for running files within the suite concurrently
91
+ - `weight`: optional scheduling weight when distributing suites across workers
60
92
 
61
- ## Manifest schema
93
+ ## Schema
62
94
 
63
- See [testkit-manifest-schema.md](testkit-manifest-schema.md).
95
+ See [testkit-config-schema.md](testkit-config-schema.md).
package/lib/cli.mjs CHANGED
@@ -12,8 +12,14 @@ export function run() {
12
12
  cli
13
13
  .command("[first] [second] [third]", "Run test suites (int, e2e, dal, all)")
14
14
  .option("-s, --suite <name>", "Run specific suite(s)", { default: [] })
15
- .option("--build", "Build image from source first", { default: false })
16
15
  .option("--dir <path>", "Explicit product directory")
16
+ .option("--jobs <n>", "Number of isolated worker stacks per service", {
17
+ default: "1",
18
+ })
19
+ .option("--shard <i/n>", "Run only shard i of n at suite granularity")
20
+ .option("--framework <name>", "Filter by framework (k6, playwright, all)", {
21
+ default: "all",
22
+ })
17
23
  .action(async (first, second, third, options) => {
18
24
  // Resolve: service filter, suite type, and --dir.
19
25
  //
@@ -57,7 +63,14 @@ export function run() {
57
63
  );
58
64
  }
59
65
 
60
- const configs = loadConfigs({ dir: options.dir, service });
66
+ const allConfigs = loadConfigs({ dir: options.dir });
67
+ const configs = service
68
+ ? allConfigs.filter((config) => config.name === service)
69
+ : allConfigs;
70
+ if (service && configs.length === 0) {
71
+ const available = allConfigs.map((config) => config.name).join(", ");
72
+ throw new Error(`Service "${service}" not found. Available: ${available}`);
73
+ }
61
74
 
62
75
  // Lifecycle commands
63
76
  if (type === "status" || type === "destroy") {
@@ -69,9 +82,48 @@ export function run() {
69
82
  return;
70
83
  }
71
84
 
85
+ if (!["all", "k6", "playwright"].includes(options.framework)) {
86
+ throw new Error(
87
+ `Unknown framework "${options.framework}". Expected one of: all, k6, playwright.`
88
+ );
89
+ }
90
+
91
+ const jobs = Number.parseInt(String(options.jobs), 10);
92
+ if (!Number.isInteger(jobs) || jobs <= 0) {
93
+ throw new Error(`Invalid --jobs value "${options.jobs}". Expected a positive integer.`);
94
+ }
95
+
96
+ let shard = null;
97
+ if (options.shard) {
98
+ const match = String(options.shard).match(/^(\d+)\/(\d+)$/);
99
+ if (!match) {
100
+ throw new Error(
101
+ `Invalid --shard value "${options.shard}". Expected the form "i/n", e.g. 1/3.`
102
+ );
103
+ }
104
+ const index = Number.parseInt(match[1], 10);
105
+ const total = Number.parseInt(match[2], 10);
106
+ if (index <= 0 || total <= 0 || index > total) {
107
+ throw new Error(
108
+ `Invalid --shard value "${options.shard}". Expected 1 <= i <= n.`
109
+ );
110
+ }
111
+ shard = { index, total };
112
+ }
113
+
72
114
  const suiteType = type || "all";
73
115
  const suiteNames = Array.isArray(options.suite) ? options.suite : [options.suite].filter(Boolean);
74
- await runner.runAll(configs, suiteType, suiteNames, options);
116
+ await runner.runAll(
117
+ configs,
118
+ suiteType,
119
+ suiteNames,
120
+ {
121
+ ...options,
122
+ jobs,
123
+ shard,
124
+ },
125
+ allConfigs
126
+ );
75
127
  });
76
128
 
77
129
  cli.help();