diff --git a/scripts/mutation-gate.mjs b/scripts/mutation-gate.mjs index 12aae0fe..b956d17d 100644 --- a/scripts/mutation-gate.mjs +++ b/scripts/mutation-gate.mjs @@ -1173,6 +1173,36 @@ function dropWorktree(dir) { rmSync(dir, { recursive: true, force: true }); } +/** + * Нужен ли гварду скомпилированный `test-build/` (#235). + * + * Гварды в реестре двух видов: длинные сами начинаются с + * `npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs`, короткие — + * сразу с `node --test`. В свежем worktree каталога `test-build/` нет вообще, + * поэтому короткий гвард падал с `ERR_MODULE_NOT_FOUND` — и это читалось как + * «мутант поймал поломку», хотя означало «гвард не смог исполниться». Компиляция + * стоит секунд, поэтому шаг ставится только там, где гвард не делает его сам. + */ +export function guardNeedsTestBuild(guard) { + return /(^|[\s&|;])node --test\b/.test(guard) && !guard.includes('tsconfig.test.json'); +} + +/** + * Собрать `test-build/` из мутированного src в каталоге мутанта. + * + * Код выхода `tsc` игнорируется сознательно, по той же причине, что и в + * `buildBundle`: мутант воспроизводит поломку, а не образцовый код, и имеет + * право быть нестрогим по типам. Доказательством служит появление каталога — + * если его нет, падаем громко, а не отдаём тесту пустоту. + */ +function buildTestBuild(dir) { + sh('npx tsc -p tsconfig.test.json', dir); + const fixed = sh('node scripts/fix-test-build.mjs', dir); + if (!existsSync(join(dir, 'test-build'))) { + throw new Error(`test-build не собрался в мутанте:\n${(fixed.stderr || fixed.stdout).slice(-2000)}`); + } +} + function buildBundle(dir) { // Только rollup, без tsc --noEmit: мутант имеет право быть нестрогим по // типам — он воспроизводит поломку, а не образцовый код. @@ -1188,6 +1218,7 @@ function runMutant(mutant) { try { applyPatches(dir, mutant.patches); buildBundle(dir); + if (guardNeedsTestBuild(mutant.guard)) buildTestBuild(dir); const guard = sh(mutant.guard, dir); if (guard.status === 0) { console.log(`FAIL ${mutant.id}: тест остался зелёным на сломанном коде`); @@ -1209,6 +1240,8 @@ function runCleanGuards(mutants) { const dir = makeWorktree(); try { buildBundle(dir); + // Один worktree на все чистые гварды — значит и компиляция одна. + if (guards.some(guardNeedsTestBuild)) buildTestBuild(dir); for (const guard of guards) { const result = sh(guard, dir); if (result.status !== 0) { diff --git a/test/mutation-gate.test.mjs b/test/mutation-gate.test.mjs index 504ee2c1..82b41318 100644 --- a/test/mutation-gate.test.mjs +++ b/test/mutation-gate.test.mjs @@ -5,7 +5,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { MUTANTS, applyPatches } from '../scripts/mutation-gate.mjs'; +import { MUTANTS, applyPatches, guardNeedsTestBuild } from '../scripts/mutation-gate.mjs'; const repoRoot = fileURLToPath(new URL('..', import.meta.url)); @@ -75,3 +75,34 @@ test('applyPatches rewrites the anchor and refuses a stale one', () => { rmSync(dir, { recursive: true, force: true }); } }); + +// #235: в свежем worktree мутанта каталога test-build/ нет. Короткий гвард +// (сразу `node --test`) падал там с ERR_MODULE_NOT_FOUND, а гейт читал это как +// «мутант пойман» — тихий отказ, который девять мутантов держал мёртвыми, не +// краснея. Теперь компиляцию делает харнесс, и здесь проверяется, что он +// узнаёт нужный ему гвард. + +test('the harness compiles test-build for exactly the guards that need it (#235)', () => { + assert.equal(guardNeedsTestBuild('node --test --test-name-pattern="x" test/a.test.mjs'), true); + assert.equal(guardNeedsTestBuild( + 'npx tsc -p tsconfig.test.json && node scripts/fix-test-build.mjs && node --test test/a.test.mjs', + ), false, 'длинный гвард компилирует сам — второй раз не надо'); + assert.equal(guardNeedsTestBuild('node demo/smoke_x.mjs'), false, 'смоку хватает бандла'); + assert.equal(guardNeedsTestBuild('node scripts/backend-test-guard.mjs x'), false); +}); + +test('no mutant guard is left unable to resolve test-build (#235)', () => { + for (const mutant of MUTANTS) { + const file = mutant.guard.split(/\s+/).find((part) => part.endsWith('.test.mjs')); + if (!file) continue; + const source = readFileSync(join(repoRoot, file), 'utf8'); + if (!source.includes('test-build/')) continue; + // Либо гвард компилирует сам, либо это делает харнесс — третьего исхода + // (падение на резолве модуля) быть не должно. + assert.ok( + mutant.guard.includes('tsconfig.test.json') || guardNeedsTestBuild(mutant.guard), + `${mutant.id}: гвард читает ${file}, который импортирует test-build/, ` + + 'и никто этот каталог в мутанте не соберёт', + ); + } +});