@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 +30 -20
- package/lib/config.mjs +15 -26
- package/lib/runner.mjs +2 -1
- package/package.json +3 -2
- package/vendor/k6 +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @elench/testkit
|
|
2
2
|
|
|
3
|
-
CLI that reads `
|
|
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
|
-
|
|
16
|
+
Add platform secrets to each product's `.env`:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
NEON_API_KEY='...'
|
|
20
|
+
FLY_API_TOKEN='...'
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
Product secrets (`
|
|
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
|
-
|
|
31
|
-
node testkit/bin/testkit.mjs bourne --build
|
|
28
|
+
cd bourne
|
|
32
29
|
|
|
33
|
-
#
|
|
34
|
-
|
|
30
|
+
# Run all suites
|
|
31
|
+
npx @elench/testkit
|
|
35
32
|
|
|
36
33
|
# Specific type / suite
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
|
48
|
-
2. **Neon** — creates
|
|
49
|
-
3. **Fly** —
|
|
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. **
|
|
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
|
-
|
|
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, "
|
|
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, "
|
|
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
|
|
117
|
+
* Resolve the bundled k6-sql binary for DAL tests. Returns the absolute path.
|
|
117
118
|
*/
|
|
118
|
-
export function resolveDalBinary(
|
|
119
|
-
const
|
|
120
|
-
const
|
|
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, "
|
|
137
|
-
throw new Error(`No
|
|
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, "
|
|
138
|
+
if (fs.existsSync(path.join(cwd, "testkit.manifest.json"))) return cwd;
|
|
142
139
|
|
|
143
140
|
throw new Error(
|
|
144
|
-
`No
|
|
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, "
|
|
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
|
-
|
|
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(
|
|
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
|
+
"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
|