mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
- .githooks/pre-push + scripts/pre-push-gate.mjs --hook: gate:small runs by default for issue/* branches with an executable diff; C/D-only diffs and non-issue refs are skipped, HP_PREPUSH_GATE=0 turns it off, =1 forces it. A non-HEAD push with an executable diff is rejected (the gate checks the working tree). The gate runs with every GIT_* variable removed: git gives hooks GIT_DIR (and GIT_CONFIG_PARAMETERS with the pusher's -c), and unit tests that `git init` temporary repositories otherwise write into the real one — seen on the first live run. The manual #343 mode is unchanged. - scripts/sandbox-bootstrap.sh: idempotent worktree / deps / chromium (npm @sparticuz/chromium@152.0.0 + Playwright shims) / bundle / check steps, no owner paths or credentials. - scripts/test-chunk.mjs, npm run test:chunk -- N/M: round-robin over test/*.test.mjs sorted by code point. - Tests, seven mutants, docs/DEVELOPMENT.md «Локальный контур за 5 минут», docs/TESTING.md. package.json changed → bundle rebuilt (fingerprint input). Issue: #633 User-Visible: no
52 lines
3.1 KiB
JavaScript
52 lines
3.1 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
|
|
import { chunkFiles, listTestFiles, parseChunk } from '../scripts/test-chunk.mjs';
|
|
|
|
// #633. Полный набор юнитов в песочнице агента не укладывается в одну команду
|
|
// (≈3 мин), и его резали руками. `npm run test:chunk -- N/M` делит детерминированно;
|
|
// эти тесты держат главное свойство: M частей вместе — каждый файл ровно один раз.
|
|
|
|
test('N/M разбирается, невозможная часть — ошибка, а не пустой прогон (#633)', () => {
|
|
assert.deepEqual(parseChunk('2/6'), { index: 2, total: 6 });
|
|
assert.deepEqual(parseChunk(' 1/1 '), { index: 1, total: 1 });
|
|
for (const bad of ['0/4', '5/4', '1/0', '2', '', undefined, 'a/b', '-1/4', '1/4/2']) {
|
|
assert.throws(() => parseChunk(bad), /N\/M|не существует/, String(bad));
|
|
}
|
|
});
|
|
|
|
test('каждый test/*.test.mjs ровно в одной части при любом M (#633 AC3)', () => {
|
|
const files = listTestFiles();
|
|
assert.ok(files.length > 10, 'набор тестов найден');
|
|
assert.ok(files.includes('test/test-chunk.test.mjs'));
|
|
assert.ok(files.every((file) => /^test\/[^/]+\.test\.mjs$/.test(file)));
|
|
for (const total of [1, 2, 3, 4, 6, 7, files.length, files.length + 3]) {
|
|
const seen = new Map();
|
|
for (let index = 1; index <= total; index += 1) {
|
|
for (const file of chunkFiles(files, index, total)) seen.set(file, (seen.get(file) ?? 0) + 1);
|
|
}
|
|
assert.deepEqual([...seen.keys()].sort(), [...files].sort(), `M=${total}: покрыты все файлы`);
|
|
assert.ok([...seen.values()].every((count) => count === 1), `M=${total}: ни один файл не попал дважды`);
|
|
}
|
|
});
|
|
|
|
test('деление детерминировано и не зависит от порядка входа; части равны ±1 (#633)', () => {
|
|
const files = Array.from({ length: 23 }, (_, i) => `test/f${String(i).padStart(2, '0')}.test.mjs`);
|
|
const shuffled = [...files].reverse();
|
|
for (let index = 1; index <= 4; index += 1) {
|
|
assert.deepEqual(chunkFiles(shuffled, index, 4), chunkFiles(files, index, 4));
|
|
}
|
|
assert.deepEqual(chunkFiles(files, 1, 4).slice(0, 3), ['test/f00.test.mjs', 'test/f04.test.mjs', 'test/f08.test.mjs']);
|
|
const sizes = [1, 2, 3, 4].map((index) => chunkFiles(files, index, 4).length);
|
|
assert.ok(Math.max(...sizes) - Math.min(...sizes) <= 1, sizes.join(','));
|
|
// Сортировка по коду символов, не по локали: одна и та же на любой машине.
|
|
assert.deepEqual(chunkFiles(['test/b.test.mjs', 'test/B.test.mjs', 'test/a.test.mjs'], 1, 1),
|
|
['test/B.test.mjs', 'test/a.test.mjs', 'test/b.test.mjs']);
|
|
});
|
|
|
|
test('npm run test:chunk ведёт в scripts/test-chunk.mjs (#633)', () => {
|
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
assert.equal(pkg.scripts['test:chunk'], 'node scripts/test-chunk.mjs');
|
|
});
|