From 0f5161164fa0dd9454bead4128b8b5baeda21012 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 2 Sep 2026 22:31:28 +0300 Subject: [PATCH] test: prove the drift gate itself can fail (#422) User-Visible: no Issue: #422 --- scripts/mutation-gate.mjs | 24 +++++++++++++ test/capture-determinism-gate.test.mjs | 50 ++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 test/capture-determinism-gate.test.mjs diff --git a/scripts/mutation-gate.mjs b/scripts/mutation-gate.mjs index ac4c15c0..c593df10 100644 --- a/scripts/mutation-gate.mjs +++ b/scripts/mutation-gate.mjs @@ -956,6 +956,30 @@ const MUTANT_DEFINITIONS = [ replace: '', }], }, + { + id: 'capture-gate-forgives-a-missing-frame', + guard: 'node --test test/capture-determinism-gate.test.mjs', + because: 'comparing only the frames both runs happen to have lets a run that ' + + 'produced nine frames pass against a run that produced ten — «совпало» then ' + + 'means nothing, which is the opposite of what the gate is for (#422)', + patches: [{ + file: 'scripts/capture-determinism.mjs', + find: ' const names = [...new Set([...Object.keys(first), ...Object.keys(second)])].sort();', + replace: ' const names = Object.keys(first).filter((name) => name in second).sort();', + }], + }, + { + id: 'capture-gate-hashes-everything-in-the-folder', + guard: 'node --test test/capture-determinism-gate.test.mjs', + because: 'hashing every file in docs/images drags the manifest into the comparison, ' + + 'and the manifest legitimately changes between runs — the gate would then cry ' + + 'wolf on every capture and be switched off within a week (#422)', + patches: [{ + file: 'scripts/capture-determinism.mjs', + find: " if (!name.endsWith('.png')) continue;\n", + replace: '', + }], + }, { id: 'capture-drifts-between-runs', guard: 'node --test test/capture-clip.test.mjs', diff --git a/test/capture-determinism-gate.test.mjs b/test/capture-determinism-gate.test.mjs new file mode 100644 index 00000000..6ea379b2 --- /dev/null +++ b/test/capture-determinism-gate.test.mjs @@ -0,0 +1,50 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { driftBetweenRuns, frameHashes } from '../scripts/capture-determinism.mjs'; + +// #422. Гейт сравнивает два прогона съёмки. Его собственная логика должна +// уметь падать не меньше, чем то, что он охраняет: разошедшийся кадр, кадр +// пропавший и кадр появившийся — всё это расхождения, потому что набор обязан +// быть одним и тем же, иначе «совпало» ничего не значит. + +const files = { 'a.png': 'AAA', 'b.png': 'BBB', 'notes.txt': 'ignore me' }; +const fakeList = () => Object.keys(files); +const fakeRead = (path) => Buffer.from(files[path.split('/').pop()]); + +test('хешируются только кадры, посторонние файлы не участвуют', () => { + const hashes = frameHashes('/nowhere', fakeRead, fakeList); + assert.deepEqual(Object.keys(hashes), ['a.png', 'b.png']); + assert.match(hashes['a.png'], /^[0-9a-f]{64}$/); + assert.notEqual(hashes['a.png'], hashes['b.png']); +}); + +test('одинаковые прогоны расхождений не дают', () => { + const first = frameHashes('/nowhere', fakeRead, fakeList); + assert.deepEqual(driftBetweenRuns(first, { ...first }), []); +}); + +test('разошедшийся кадр назван по имени', () => { + const drift = driftBetweenRuns({ 'a.png': '1', 'b.png': '2' }, { 'a.png': '1', 'b.png': '9' }); + assert.deepEqual(drift.map((item) => item.name), ['b.png']); + assert.equal(drift[0].first, '2'); + assert.equal(drift[0].second, '9'); +}); + +test('пропавший и появившийся кадр — тоже расхождение', () => { + const lost = driftBetweenRuns({ 'a.png': '1', 'b.png': '2' }, { 'a.png': '1' }); + assert.deepEqual(lost.map((item) => item.name), ['b.png']); + assert.equal(lost[0].second, '(нет кадра)'); + + const appeared = driftBetweenRuns({ 'a.png': '1' }, { 'a.png': '1', 'c.png': '3' }); + assert.deepEqual(appeared.map((item) => item.name), ['c.png']); + assert.equal(appeared[0].first, '(нет кадра)'); +}); + +test('расхождения перечисляются в устойчивом порядке', () => { + const drift = driftBetweenRuns( + { 'c.png': '1', 'a.png': '1', 'b.png': '1' }, + { 'c.png': '9', 'a.png': '9', 'b.png': '9' }, + ); + assert.deepEqual(drift.map((item) => item.name), ['a.png', 'b.png', 'c.png']); +});