n8n/scripts/grind.mjs
n8n-cat-bot[bot] 6f26691405
ci: Add pre-merge flake-detection workflow (no-changelog) (#31095)
Co-authored-by: n8n-cat-bot[bot] <n8n-cat-bot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Declan Carroll <declan@n8n.io>
2026-05-26 13:15:28 +00:00

117 lines
3.5 KiB
JavaScript

#!/usr/bin/env node
/**
* Grinds a single vitest file N times in a fresh process per iteration.
*
* Spawning per iteration (rather than `vitest --repeat=N`) is intentional:
* the flakes we target manifest during worker teardown, which only fires
* at process exit. See DEVP-198 for design notes.
*
* Usage:
* pnpm grind <test-file> [n]
* node scripts/grind.mjs --file <path> --n 10 [--json]
*
* Exit code: 0 when all iterations pass, 1 otherwise.
*/
import { spawnSync } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import { dirname, isAbsolute, relative, resolve } from 'node:path';
import { parseArgs } from 'node:util';
const { values, positionals } = parseArgs({
options: {
file: { type: 'string' },
n: { type: 'string' },
json: { type: 'boolean', default: false },
},
allowPositionals: true,
strict: true,
});
const fileArg = values.file ?? positionals[0];
const n = Number(values.n ?? positionals[1] ?? 10);
if (!fileArg) {
console.error('usage: grind.mjs <test-file> [n]');
process.exit(2);
}
const absFile = isAbsolute(fileArg) ? fileArg : resolve(process.cwd(), fileArg);
if (!existsSync(absFile)) {
console.error(`No such file: ${fileArg}`);
process.exit(2);
}
/**
* Walk up from the test file to the nearest package.json so vitest runs
* inside the package and picks up its config + module aliases. Running
* `pnpm vitest run <file>` at the monorepo root silently skips the
* package's vitest config and breaks `@/...` imports.
*/
function findPackageRoot(filePath) {
let dir = dirname(filePath);
const stopAt = process.cwd();
while (true) {
if (existsSync(resolve(dir, 'package.json'))) return dir;
const parent = dirname(dir);
if (parent === dir || dir === stopAt) return null;
dir = parent;
}
}
const pkgRoot = findPackageRoot(absFile);
if (!pkgRoot) {
console.error(`Could not find a package.json above ${absFile}`);
process.exit(2);
}
const fileRelToPkg = relative(pkgRoot, absFile);
function detectRunner(pkgRootDir) {
const pkg = JSON.parse(readFileSync(resolve(pkgRootDir, 'package.json'), 'utf8'));
const deps = { ...pkg.devDependencies, ...pkg.dependencies };
if (deps.vitest) return 'vitest';
if (deps.jest) return 'jest';
const testScript = pkg.scripts?.test ?? '';
if (/\bvitest\b/.test(testScript)) return 'vitest';
if (/\bjest\b/.test(testScript)) return 'jest';
if (existsSync(resolve(pkgRootDir, 'jest.config.js')) ||
existsSync(resolve(pkgRootDir, 'jest.config.ts')) ||
existsSync(resolve(pkgRootDir, 'jest.config.mjs'))) return 'jest';
if (existsSync(resolve(pkgRootDir, 'vitest.config.ts')) ||
existsSync(resolve(pkgRootDir, 'vitest.config.js')) ||
existsSync(resolve(pkgRootDir, 'vitest.config.mjs'))) return 'vitest';
return null;
}
const runner = detectRunner(pkgRoot);
if (!runner) {
console.error(`Could not detect vitest or jest in ${pkgRoot}/package.json`);
process.exit(2);
}
const runnerArgs =
runner === 'vitest'
? ['vitest', 'run', fileRelToPkg, '--reporter=dot']
: ['jest', fileRelToPkg, '--colors=false'];
let passed = 0;
for (let i = 0; i < n; i++) {
const res = spawnSync('pnpm', runnerArgs, {
cwd: pkgRoot,
stdio: values.json ? ['ignore', 'ignore', 'ignore'] : ['ignore', 'inherit', 'inherit'],
});
if (res.status === 0) passed++;
if (!values.json) process.stdout.write(res.status === 0 ? '.' : 'F');
}
if (values.json) {
process.stdout.write(JSON.stringify({ file: fileArg, passed, total: n }) + '\n');
} else {
console.log(`\n${passed}/${n} passed`);
}
process.exit(passed === n ? 0 : 1);