mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
109 lines
5.6 KiB
JavaScript
109 lines
5.6 KiB
JavaScript
import assert from 'node:assert/strict';
|
||
import test from 'node:test';
|
||
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||
import { tmpdir } from 'node:os';
|
||
import { join } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
import { MUTANTS, applyPatches, guardNeedsTestBuild } from '../scripts/mutation-gate.mjs';
|
||
|
||
const repoRoot = fileURLToPath(new URL('..', import.meta.url));
|
||
|
||
// Дешёвая половина гейта, идёт с обычными юнитами на каждом прогоне. Полный
|
||
// прогон с пересборкой бандла на мутанта — предрелизный, он в
|
||
// .github/workflows/mutation-gate.yml.
|
||
//
|
||
// Реестр, отставший от кода, хуже отсутствующего: он выглядит защитой. Поэтому
|
||
// дрейф якорей ловится здесь, а не при редком полном прогоне.
|
||
|
||
test('every mutant patch anchors exactly once in the current source', () => {
|
||
for (const mutant of MUTANTS) {
|
||
for (const patch of mutant.patches) {
|
||
const path = join(repoRoot, patch.file);
|
||
assert.ok(existsSync(path), `${mutant.id}: файла ${patch.file} больше нет`);
|
||
const source = readFileSync(path, 'utf8');
|
||
const hits = source.split(patch.find).length - 1;
|
||
assert.equal(hits, 1,
|
||
`${mutant.id}: якорь в ${patch.file} найден ${hits} раз(а) — реестр отстал от кода`);
|
||
assert.notEqual(patch.find, patch.replace, `${mutant.id}: патч ничего не меняет`);
|
||
}
|
||
}
|
||
});
|
||
|
||
test('every guard command points at a file that exists', () => {
|
||
for (const mutant of MUTANTS) {
|
||
const script = mutant.guard.split(' ').find((part) => part.endsWith('.mjs'));
|
||
assert.ok(script, `${mutant.id}: guard не называет исполняемый файл`);
|
||
assert.ok(existsSync(join(repoRoot, script)),
|
||
`${mutant.id}: guard-файла ${script} не существует`);
|
||
}
|
||
});
|
||
|
||
test('every mutant explains itself', () => {
|
||
const ids = new Set();
|
||
for (const mutant of MUTANTS) {
|
||
assert.ok(mutant.because && mutant.because.length > 40,
|
||
`${mutant.id}: без объяснения мутант превратится в карго-культ`);
|
||
assert.ok(!ids.has(mutant.id), `дубль id: ${mutant.id}`);
|
||
ids.add(mutant.id);
|
||
}
|
||
assert.ok(MUTANTS.length >= 6, 'стартовый набор — шесть мутантов по дырам из #85');
|
||
});
|
||
|
||
test('applyPatches rewrites the anchor and refuses a stale one', () => {
|
||
const dir = mkdtempSync(join(tmpdir(), 'hp-mg-'));
|
||
try {
|
||
mkdirSync(join(dir, 'src'), { recursive: true });
|
||
writeFileSync(join(dir, 'src', 'a.ts'), 'const KEEP = 1;\nconst FEATHER = 2;\n');
|
||
|
||
applyPatches(dir, [{ file: 'src/a.ts', find: 'const FEATHER = 2;', replace: 'const FEATHER = 20;' }]);
|
||
assert.match(readFileSync(join(dir, 'src', 'a.ts'), 'utf8'), /FEATHER = 20/);
|
||
|
||
// Якоря нет — отказ, а не тихий пропуск: патч «в никуда» выглядит защитой.
|
||
assert.throws(
|
||
() => applyPatches(dir, [{ file: 'src/a.ts', find: 'no such anchor', replace: 'x' }]),
|
||
/0 раз/,
|
||
);
|
||
|
||
// Якорь двоится — тоже отказ: патч лёг бы «куда попало».
|
||
writeFileSync(join(dir, 'src', 'a.ts'), 'twice\ntwice\n');
|
||
assert.throws(
|
||
() => applyPatches(dir, [{ file: 'src/a.ts', find: 'twice', replace: 'x' }]),
|
||
/2 раз/,
|
||
);
|
||
} finally {
|
||
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/, `
|
||
+ 'и никто этот каталог в мутанте не соберёт',
|
||
);
|
||
}
|
||
});
|