@elench/testkit 0.1.6 → 0.1.7
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/infra/neon-up.sh +1 -0
- package/lib/config.mjs +11 -0
- package/lib/runner.mjs +99 -10
- package/package.json +1 -1
package/infra/neon-up.sh
CHANGED
|
@@ -106,6 +106,7 @@ if command -v psql &>/dev/null; then
|
|
|
106
106
|
SELECT tablename FROM pg_tables
|
|
107
107
|
WHERE schemaname = 'public'
|
|
108
108
|
AND tablename NOT LIKE '%migration%'
|
|
109
|
+
AND tablename NOT LIKE 'goose_%'
|
|
109
110
|
AND tablename NOT LIKE 'drizzle_%'
|
|
110
111
|
) LOOP
|
|
111
112
|
EXECUTE 'TRUNCATE TABLE public.' || quote_ident(r.tablename) || ' CASCADE';
|
package/lib/config.mjs
CHANGED
|
@@ -178,6 +178,14 @@ function validateService(name, svc, manifestPath) {
|
|
|
178
178
|
optionalStringArray(errors, tk.fly, `${ctx}: testkit.fly.secrets`, "secrets");
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
if (tk.migrate !== undefined) {
|
|
182
|
+
if (!isObject(tk.migrate)) {
|
|
183
|
+
errors.push(`${ctx}: testkit.migrate must be an object`);
|
|
184
|
+
} else {
|
|
185
|
+
requireString(errors, tk.migrate, `${ctx}: testkit.migrate.cmd`, "cmd");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
181
189
|
if (tk.k6 !== undefined && !isObject(tk.k6)) {
|
|
182
190
|
errors.push(`${ctx}: testkit.k6 must be an object`);
|
|
183
191
|
}
|
|
@@ -201,6 +209,9 @@ function validateService(name, svc, manifestPath) {
|
|
|
201
209
|
const prefix = `${ctx}: suites.${type}[${i}]`;
|
|
202
210
|
requireString(errors, suite, `${prefix}.name`, "name");
|
|
203
211
|
requireNonEmptyStringArray(errors, suite, `${prefix}.files`, "files");
|
|
212
|
+
if (suite.pre !== undefined && (typeof suite.pre !== "string" || suite.pre.length === 0)) {
|
|
213
|
+
errors.push(`${prefix}.pre must be a non-empty string (shell command)`);
|
|
214
|
+
}
|
|
204
215
|
}
|
|
205
216
|
}
|
|
206
217
|
}
|
package/lib/runner.mjs
CHANGED
|
@@ -181,6 +181,66 @@ export async function runDalTests(config, files) {
|
|
|
181
181
|
return { failed };
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Delete the Neon branch (so neonUp creates a fresh fork from prod).
|
|
186
|
+
*/
|
|
187
|
+
async function neonReset(config) {
|
|
188
|
+
const { stateDir, manifest } = config;
|
|
189
|
+
const tk = manifest.testkit;
|
|
190
|
+
console.log("Deleting Neon branch for fresh fork...");
|
|
191
|
+
await runScript("neon-down.sh", {
|
|
192
|
+
NEON_PROJECT_ID: tk.neon.projectId,
|
|
193
|
+
STATE_DIR: stateDir,
|
|
194
|
+
});
|
|
195
|
+
// Clear branch state so neonUp creates a new one
|
|
196
|
+
const branchFile = path.join(stateDir, "neon_branch_id");
|
|
197
|
+
if (fs.existsSync(branchFile)) fs.unlinkSync(branchFile);
|
|
198
|
+
const dbUrlFile = path.join(stateDir, "database_url");
|
|
199
|
+
if (fs.existsSync(dbUrlFile)) fs.unlinkSync(dbUrlFile);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Run database migrations from local source against the test branch.
|
|
204
|
+
* If migrations fail, nukes the Neon branch and retries on a fresh fork.
|
|
205
|
+
* A second failure means a real migration bug — throws.
|
|
206
|
+
*/
|
|
207
|
+
async function migrate(config) {
|
|
208
|
+
const { productDir, stateDir, manifest } = config;
|
|
209
|
+
const migrateCmd = manifest.testkit.migrate?.cmd;
|
|
210
|
+
if (!migrateCmd) return;
|
|
211
|
+
|
|
212
|
+
const dbUrlPath = path.join(stateDir, "database_url");
|
|
213
|
+
const env = { ...process.env };
|
|
214
|
+
if (fs.existsSync(dbUrlPath)) {
|
|
215
|
+
env.DATABASE_URL = fs.readFileSync(dbUrlPath, "utf8").trim();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const execOpts = { stdio: "inherit", cwd: productDir, env, shell: true };
|
|
219
|
+
|
|
220
|
+
console.log("\n── migrate ──");
|
|
221
|
+
try {
|
|
222
|
+
await execaCommand(migrateCmd, execOpts);
|
|
223
|
+
return;
|
|
224
|
+
} catch (err) {
|
|
225
|
+
console.log("\nMigration failed — resetting Neon branch and retrying...\n");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Nuke and retry: fresh fork from prod, clean goose state
|
|
229
|
+
await neonReset(config);
|
|
230
|
+
await neonUp(config);
|
|
231
|
+
|
|
232
|
+
// Re-read DATABASE_URL from fresh branch
|
|
233
|
+
if (fs.existsSync(dbUrlPath)) {
|
|
234
|
+
env.DATABASE_URL = fs.readFileSync(dbUrlPath, "utf8").trim();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
await execaCommand(migrateCmd, execOpts);
|
|
239
|
+
} catch (retryErr) {
|
|
240
|
+
throw new Error(`Migration failed on fresh branch — likely a bug in your migration SQL:\n${retryErr.message}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
184
244
|
/**
|
|
185
245
|
* Show state directory contents.
|
|
186
246
|
*/
|
|
@@ -199,6 +259,22 @@ export function showStatus(config) {
|
|
|
199
259
|
}
|
|
200
260
|
}
|
|
201
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Run a suite's `pre` command if defined.
|
|
264
|
+
* Executes in the product directory with DATABASE_URL available.
|
|
265
|
+
*/
|
|
266
|
+
async function runSuitePre(config, suite) {
|
|
267
|
+
if (!suite.pre) return;
|
|
268
|
+
const { productDir, stateDir } = config;
|
|
269
|
+
const dbUrlPath = path.join(stateDir, "database_url");
|
|
270
|
+
const env = { ...process.env };
|
|
271
|
+
if (fs.existsSync(dbUrlPath)) {
|
|
272
|
+
env.DATABASE_URL = fs.readFileSync(dbUrlPath, "utf8").trim();
|
|
273
|
+
}
|
|
274
|
+
console.log(`\n── pre: ${suite.name} ──`);
|
|
275
|
+
await execaCommand(suite.pre, { stdio: "inherit", cwd: productDir, env, shell: true });
|
|
276
|
+
}
|
|
277
|
+
|
|
202
278
|
/**
|
|
203
279
|
* Run a single service: orchestrate HTTP and/or DAL test flows.
|
|
204
280
|
* Returns true if any tests failed.
|
|
@@ -206,25 +282,28 @@ export function showStatus(config) {
|
|
|
206
282
|
async function runService(config, suiteType, suiteNames, opts) {
|
|
207
283
|
const { manifest, stateDir } = config;
|
|
208
284
|
|
|
209
|
-
// Collect
|
|
285
|
+
// Collect suites from manifest, partitioned by flow
|
|
210
286
|
const types = suiteType === "all"
|
|
211
287
|
? Object.keys(manifest.suites)
|
|
212
288
|
: [suiteType === "int" ? "integration" : suiteType];
|
|
213
289
|
|
|
214
|
-
let
|
|
215
|
-
let
|
|
290
|
+
let httpSuites = [];
|
|
291
|
+
let dalSuites = [];
|
|
216
292
|
for (const type of types) {
|
|
217
293
|
const suites = manifest.suites[type] || [];
|
|
218
294
|
for (const suite of suites) {
|
|
219
295
|
if (suiteNames.length && !suiteNames.includes(suite.name)) continue;
|
|
220
296
|
if (type === "dal") {
|
|
221
|
-
|
|
297
|
+
dalSuites.push(suite);
|
|
222
298
|
} else {
|
|
223
|
-
|
|
299
|
+
httpSuites.push(suite);
|
|
224
300
|
}
|
|
225
301
|
}
|
|
226
302
|
}
|
|
227
303
|
|
|
304
|
+
const httpFiles = httpSuites.flatMap((s) => s.files);
|
|
305
|
+
const dalFiles = dalSuites.flatMap((s) => s.files);
|
|
306
|
+
|
|
228
307
|
if (!httpFiles.length && !dalFiles.length) {
|
|
229
308
|
console.log(`No test files for ${config.name} type="${suiteType}" suites=${suiteNames.join(",") || "all"} — skipping`);
|
|
230
309
|
return false;
|
|
@@ -235,14 +314,18 @@ async function runService(config, suiteType, suiteNames, opts) {
|
|
|
235
314
|
let failed = false;
|
|
236
315
|
let neonReady = false;
|
|
237
316
|
|
|
238
|
-
// HTTP flow: build → neon → fly → run → fly down
|
|
317
|
+
// HTTP flow: build → neon → migrate → fly → run → fly down
|
|
239
318
|
if (httpFiles.length) {
|
|
240
319
|
requireFlyToken(config.name);
|
|
241
320
|
if (opts.build) await build(config);
|
|
242
321
|
await neonUp(config);
|
|
322
|
+
await migrate(config);
|
|
243
323
|
neonReady = true;
|
|
244
324
|
await flyUp(config);
|
|
245
325
|
try {
|
|
326
|
+
for (const suite of httpSuites) {
|
|
327
|
+
await runSuitePre(config, suite);
|
|
328
|
+
}
|
|
246
329
|
const result = await runTests(config, httpFiles);
|
|
247
330
|
if (result?.failed) failed = true;
|
|
248
331
|
} finally {
|
|
@@ -250,11 +333,17 @@ async function runService(config, suiteType, suiteNames, opts) {
|
|
|
250
333
|
}
|
|
251
334
|
}
|
|
252
335
|
|
|
253
|
-
// DAL flow: neon
|
|
336
|
+
// DAL flow: neon → migrate → run suite pre hooks → run DAL tests
|
|
254
337
|
if (dalFiles.length) {
|
|
255
|
-
if (!neonReady)
|
|
256
|
-
|
|
257
|
-
|
|
338
|
+
if (!neonReady) {
|
|
339
|
+
await neonUp(config);
|
|
340
|
+
await migrate(config);
|
|
341
|
+
}
|
|
342
|
+
for (const suite of dalSuites) {
|
|
343
|
+
await runSuitePre(config, suite);
|
|
344
|
+
const result = await runDalTests(config, suite.files);
|
|
345
|
+
if (result?.failed) failed = true;
|
|
346
|
+
}
|
|
258
347
|
}
|
|
259
348
|
|
|
260
349
|
return failed;
|