fix: Harden mutation picker against low-value source files (#31914)

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>
This commit is contained in:
n8n-cat-bot[bot]
2026-06-08 22:53:53 +01:00
committed by GitHub
parent 2a88b50360
commit 7e5c5c4eac
+37 -3
View File
@@ -38,7 +38,7 @@
*/
import { readdir, readFile } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { existsSync, readFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import path from 'node:path';
@@ -65,20 +65,54 @@ function parseArgs(argv) {
}
// Files with no useful mutation surface: barrels, declarations, type-only modules.
const LOW_VALUE_BASENAMES = new Set(['interfaces', 'index', 'constants', 'types']);
// Matched against either the full basename (`types.ts`) or the trailing
// dot-segment (`foo.types.ts` → `types`), so dotted-suffix declaration files
// are caught the same as their plain-named counterparts. `methods` covers
// NativeDoc `*.methods.ts` descriptor files; `schemas` covers pure Zod
// declaration files; `message-event-bus` is an exact-basename entry for a
// bulk enum + interfaces + Zod module with no function logic.
const LOW_VALUE_BASENAMES = new Set([
'interfaces',
'index',
'constants',
'types',
'methods',
'schemas',
'message-event-bus',
]);
// Files with fewer than this many non-blank, non-import lines have so little
// surface area they're not worth mutating — e.g. trivial error subclasses with
// empty bodies or one hardcoded super() call. This catches what an exact-name
// list can't (`error` can't be skipped wholesale because files like
// `workflow-activation.error.ts` have real branching logic).
const MIN_MEANINGFUL_LINES = 15;
// Directories whose contents are tests/fixtures/mocks, not production code.
// Pruned in `walkSources` so we don't descend; `isMutationWorthy` re-checks as a
// safety net for packages that co-locate tests as siblings (e.g. `foo.test.ts`).
const NON_SOURCE_DIRS = new Set(['__tests__', '__mocks__', 'fixtures']);
function countMeaningfulLines(content) {
let count = 0;
for (const raw of content.split('\n')) {
const line = raw.trim();
if (!line) continue;
if (line.startsWith('import ')) continue;
count++;
}
return count;
}
function isMutationWorthy(absPath) {
if (absPath.endsWith('.d.ts')) return false;
if (/\.(test|spec)\.ts$/.test(absPath)) return false;
if (absPath.includes(`${path.sep}__tests__${path.sep}`)) return false;
if (absPath.includes(`${path.sep}__mocks__${path.sep}`)) return false;
const base = path.basename(absPath, '.ts');
if (LOW_VALUE_BASENAMES.has(base)) return false;
const lastSegment = base.split('.').at(-1);
if (LOW_VALUE_BASENAMES.has(base) || LOW_VALUE_BASENAMES.has(lastSegment)) return false;
if (countMeaningfulLines(readFileSync(absPath, 'utf8')) < MIN_MEANINGFUL_LINES) return false;
return true;
}