fix: compile test-build inside the mutant worktree

Issue: #235
User-Visible: no
This commit is contained in:
Matysh
2026-08-22 11:24:37 +03:00
parent 6bd197ffd7
commit 3aba493b25
2 changed files with 65 additions and 1 deletions
+33
View File
@@ -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) {
+32 -1
View File
@@ -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/, `
+ 'и никто этот каталог в мутанте не соберёт',
);
}
});