@elench/testkit 0.1.3 → 0.1.5

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,6 +1,6 @@
1
1
  # @elench/testkit
2
2
 
3
- CLI that reads `runner.manifest.json` from a product repo, spins up an ephemeral environment (Neon DB branch + Fly machine), runs k6 tests, and tears down.
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.
4
4
 
5
5
  ## Prerequisites
6
6
 
@@ -13,41 +13,51 @@ fly auth login
13
13
 
14
14
  ## Setup
15
15
 
16
- Set platform secrets in your shell (e.g. `.envrc`):
16
+ Add platform secrets to each product's `.env`:
17
17
 
18
18
  ```bash
19
- export NEON_API_KEY='...'
20
- export FLY_API_TOKEN='...'
19
+ NEON_API_KEY='...'
20
+ FLY_API_TOKEN='...'
21
21
  ```
22
22
 
23
- Product secrets (`JWT_SECRET`, `ENCRYPTION_KEY`, etc.) are loaded from each product's `.env`.
23
+ Product secrets (`CLERK_SECRET_KEY`, etc.) are also loaded from the same `.env`.
24
24
 
25
25
  ## Usage
26
26
 
27
- From the `elench/` workspace root:
28
-
29
27
  ```bash
30
- # Full run — build, create env, test, stop machine
31
- node testkit/bin/testkit.mjs bourne --build
28
+ cd bourne
32
29
 
33
- # Fast re-run — reuse machine + branch, reset tables
34
- node testkit/bin/testkit.mjs bourne
30
+ # Run all suites
31
+ npx @elench/testkit
35
32
 
36
33
  # Specific type / suite
37
- node testkit/bin/testkit.mjs bourne int -s health
38
- node testkit/bin/testkit.mjs outreach e2e
34
+ npx @elench/testkit int -s health
35
+ npx @elench/testkit e2e
36
+
37
+ # Specific service (multi-service products)
38
+ npx @elench/testkit avocado_api int -s health
39
+
40
+ # Build from source first
41
+ npx @elench/testkit int --build
39
42
 
40
43
  # Lifecycle
41
- node testkit/bin/testkit.mjs bourne status
42
- node testkit/bin/testkit.mjs bourne destroy
44
+ npx @elench/testkit status
45
+ npx @elench/testkit destroy
43
46
  ```
44
47
 
45
48
  ## How it works
46
49
 
47
- 1. **Config** — reads `<product>/runner.manifest.json` for infra config + test suites
48
- 2. **Neon** — creates/reuses a persistent branch, truncates tables between runs
49
- 3. **Fly** — builds image (if `--build`), creates/reuses a machine with the right env vars
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
50
53
  4. **k6** — runs matched test files with `BASE_URL` and `MACHINE_ID` injected
51
- 5. **Cleanup** — stops the Fly machine (preserved for next run)
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)
56
+
57
+ Multi-service products run all services in parallel. Each service gets its own Neon branch and Fly machine.
58
+
59
+ State is persisted in `.testkit/` (or `.testkit/<service>/` for multi-service) so subsequent runs reuse existing infrastructure.
60
+
61
+ ## Manifest schema
52
62
 
53
- State is persisted in `<product>/.testkit/` so subsequent runs skip setup.
63
+ See [testkit-manifest-schema.md](testkit-manifest-schema.md).
package/lib/config.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
+ import { fileURLToPath } from "url";
3
4
 
4
5
  /**
5
6
  * Parse a .env file into an object. Supports KEY=VALUE, KEY='VALUE', KEY="VALUE".
@@ -30,7 +31,7 @@ export function parseDotenv(filePath) {
30
31
  */
31
32
  export function getServiceNames(cwd) {
32
33
  const dir = cwd || process.cwd();
33
- const manifestPath = path.join(dir, "runner.manifest.json");
34
+ const manifestPath = path.join(dir, "testkit.manifest.json");
34
35
  if (!fs.existsSync(manifestPath)) return [];
35
36
  const m = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
36
37
  if (!isObject(m.services)) return [];
@@ -47,7 +48,7 @@ export function getServiceNames(cwd) {
47
48
  export function loadConfigs(opts = {}) {
48
49
  const cwd = process.cwd();
49
50
  const productDir = resolveProductDir(cwd, opts.dir);
50
- const manifestPath = path.join(productDir, "runner.manifest.json");
51
+ const manifestPath = path.join(productDir, "testkit.manifest.json");
51
52
  const raw = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
52
53
 
53
54
  if (!isObject(raw.services)) {
@@ -113,17 +114,13 @@ export function requireFlyToken(serviceName) {
113
114
  }
114
115
 
115
116
  /**
116
- * Resolve the custom k6 binary for DAL tests. Returns the absolute path.
117
+ * Resolve the bundled k6-sql binary for DAL tests. Returns the absolute path.
117
118
  */
118
- export function resolveDalBinary(config) {
119
- const { productDir, manifest } = config;
120
- const rel = manifest.testkit.dal?.k6Binary;
121
- if (!rel) {
122
- throw new Error("testkit.dal.k6Binary is required for DAL tests");
123
- }
124
- const abs = path.resolve(productDir, rel);
119
+ export function resolveDalBinary() {
120
+ const thisFile = fileURLToPath(import.meta.url);
121
+ const abs = path.resolve(path.dirname(thisFile), "..", "vendor", "k6");
125
122
  if (!fs.existsSync(abs)) {
126
- throw new Error(`DAL k6 binary not found: ${abs}`);
123
+ throw new Error(`Bundled DAL k6 binary not found: ${abs}`);
127
124
  }
128
125
  return abs;
129
126
  }
@@ -133,15 +130,15 @@ export function resolveDalBinary(config) {
133
130
  function resolveProductDir(cwd, explicitDir) {
134
131
  if (explicitDir) {
135
132
  const p = path.resolve(cwd, explicitDir);
136
- if (fs.existsSync(path.join(p, "runner.manifest.json"))) return p;
137
- throw new Error(`No runner.manifest.json in ${p}`);
133
+ if (fs.existsSync(path.join(p, "testkit.manifest.json"))) return p;
134
+ throw new Error(`No testkit.manifest.json in ${p}`);
138
135
  }
139
136
 
140
137
  // Check cwd
141
- if (fs.existsSync(path.join(cwd, "runner.manifest.json"))) return cwd;
138
+ if (fs.existsSync(path.join(cwd, "testkit.manifest.json"))) return cwd;
142
139
 
143
140
  throw new Error(
144
- `No runner.manifest.json in current directory. ` +
141
+ `No testkit.manifest.json in current directory. ` +
145
142
  `Either cd into a product directory or use --dir.`
146
143
  );
147
144
  }
@@ -152,7 +149,7 @@ function resolveProductDir(cwd, explicitDir) {
152
149
  */
153
150
  export function isSiblingProduct(name) {
154
151
  const candidate = path.join(process.cwd(), name);
155
- return fs.existsSync(path.join(candidate, "runner.manifest.json"));
152
+ return fs.existsSync(path.join(candidate, "testkit.manifest.json"));
156
153
  }
157
154
 
158
155
  // ── Per-service validation ──────────────────────────────────────────────
@@ -185,16 +182,8 @@ function validateService(name, svc, manifestPath) {
185
182
  errors.push(`${ctx}: testkit.k6 must be an object`);
186
183
  }
187
184
 
188
- if (tk.dal !== undefined) {
189
- if (!isObject(tk.dal)) {
190
- errors.push(`${ctx}: testkit.dal must be an object`);
191
- } else if (svc.suites?.dal) {
192
- requireString(errors, tk.dal, `${ctx}: testkit.dal.k6Binary`, "k6Binary");
193
- }
194
- }
195
-
196
- if (svc.suites?.dal && !tk.dal) {
197
- errors.push(`${ctx}: testkit.dal is required when suites.dal exists`);
185
+ if (tk.dal !== undefined && !isObject(tk.dal)) {
186
+ errors.push(`${ctx}: testkit.dal must be an object`);
198
187
  }
199
188
  }
200
189
 
package/lib/runner.mjs CHANGED
@@ -62,6 +62,7 @@ export async function neonUp(config) {
62
62
  await runScript("neon-up.sh", {
63
63
  NEON_PROJECT_ID: tk.neon.projectId,
64
64
  NEON_DB_NAME: tk.neon.dbName,
65
+ NEON_BRANCH_NAME: tk.neon.branchName || `${config.name}-test`,
65
66
  STATE_DIR: stateDir,
66
67
  });
67
68
  }
@@ -155,7 +156,7 @@ export async function runTests(config, files) {
155
156
  export async function runDalTests(config, files) {
156
157
  const { productDir, stateDir, manifest } = config;
157
158
  const tk = manifest.testkit;
158
- const k6Binary = resolveDalBinary(config);
159
+ const k6Binary = resolveDalBinary();
159
160
 
160
161
  // Read DATABASE_URL from neon state
161
162
  const databaseUrl = fs.readFileSync(path.join(stateDir, "database_url"), "utf8").trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elench/testkit",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "CLI for running k6 tests against real, ephemeral infrastructure",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,8 @@
9
9
  "files": [
10
10
  "bin/",
11
11
  "lib/",
12
- "infra/"
12
+ "infra/",
13
+ "vendor/"
13
14
  ],
14
15
  "dependencies": {
15
16
  "cac": "^6.7.14",
package/vendor/k6 ADDED
Binary file