mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
Validate / provenance (push) Successful in 46s
Validate / hacs (push) Failing after 10s
Validate / hassfest (push) Failing after 12s
Validate / process-gate (push) Failing after 35s
Validate / frontend (push) Successful in 6m11s
Validate / backend (push) Failing after 9m24s
Validate / golden (push) Failing after 10m10s
Validate / performance_smoke (push) Failing after 12m46s
Validate / smoke (push) Failing after 30m15s
Five times in this project a green test meant nothing was checked. The continuity smoke stayed green after the entire mechanism it guards was cut out. The golden scene created to protect doorway light was empty — 1,177 warm pixels against 107,119, all of them icons. The shadow smoke passed while no shadow was drawn. Each time the test had been written alongside the code, went green at once, and nobody ever asked whether it could go red. The gate makes that question routine. Each mutant is a few lines of patch that reproduce a known breakage, plus the name of the test that must fail on it. A worktree is patched, the bundle rebuilt, the guard run — and a guard that stays green fails the gate. Six mutants cover the holes documented in #85; the anchors are exact strings from today's source, so the registry cannot silently drift — a unit test that runs with the ordinary suite refuses a stale anchor. The full run rebuilds the bundle per mutant, so it lives in its own workflow, before a stable release and on a weekly schedule, not in Validate. The rules for new tests are written at the top of docs/TESTING.md, and the sixth of them is the cheapest: an assertion that reads back the property the code just set is not written at all. Issue: #85 User-Visible: no
78 lines
3.6 KiB
JavaScript
78 lines
3.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 } 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 });
|
|
}
|
|
});
|