Files
houseplan-card/test/ratchets.test.mjs
T
Claudeandclaude[bot] c68d92f674 process: ratchets get a band over the beta ceiling (#699)
Two-sided ratchets with zero slack made parallel tasks conflict on shared
numbers, recompute them after every rebase and hit a ceiling because a
neighbour merged first (#689 after #691).

- Core lines (test/core-file-budget.test.mjs): a branch may grow up to
  CORE_BAND = 50 lines over the beta ceiling; shrinking no longer fails it.
- Bundle graphs (bundle-budget.mjs): initial View and lazy graphs fail only
  above ceiling + 2 000 B; below the ceiling is not a branch finding. The
  absolute INITIAL_VIEW_GZIP_BUDGET stays the wall.
- Monolith numbers (monolith-metrics.mjs, unused-locals-gate.mjs):
  METRIC_BANDS — 5 for delegates, port members and privates, 25 for host.
  refs, 2 000 B for dist/; a lower number is reported, not failed.
- Browser mutation guards: 200 is a guideline — mutation-gate --check warns
  above it instead of failing; every guard still needs its reason line.
- scripts/ratchets.mjs: `report [--warn]` and `tighten` — on the beta
  candidate the release manager sets every ceiling to the fact in one
  commit; release:prerelease prints loose ceilings as a warning.

Canon: PROCESS.md §3 (browser guards, monolith numbers) and §8 «Храповики»;
docs/TESTING.md.

Issue: #699
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
2026-09-28 22:39:36 +00:00

68 lines
4.3 KiB
JavaScript

// #699, PROCESS.md §8: полоса над потолком беты вместо точки; потолки
// опускает до факта релиз-менеджер одной командой на кандидате.
import assert from 'node:assert/strict';
import test from 'node:test';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import {
BUNDLE_CEILINGS, CORE_BAND, CORE_BUDGET_FILE, BUNDLE_BUDGET_FILE, formatRow, ratchetRows, ratchetState,
readConst, readCoreCaps, rewriteConst, rewriteCoreCaps,
} from '../scripts/ratchets.mjs';
import { METRIC_NAMES } from '../scripts/monolith-metrics.mjs';
const read = (path) => readFileSync(fileURLToPath(new URL(`../${path}`, import.meta.url)), 'utf8');
test('#699 состояние храповика: равен, рыхлый, в полосе, выше полосы, не измерен', () => {
assert.equal(ratchetState(100, 100, 50), 'tight');
assert.equal(ratchetState(90, 100, 50), 'loose');
assert.equal(ratchetState(150, 100, 50), 'over', 'граница полосы включительная — как у гейтов ветки');
assert.equal(ratchetState(151, 100, 50), 'grew');
assert.equal(ratchetState(undefined, 100, 50), 'missing');
assert.equal(ratchetState(100, null, 50), 'missing');
});
test('#699 потолки ядер читаются и переписываются в исходнике теста, комментарии на месте', () => {
const source = read(CORE_BUDGET_FILE);
const caps = readCoreCaps(source);
// Имена ядер здесь не пишутся: тесты, читающие монолит как текст, заморожены (#624).
const [first, second] = Object.keys(caps);
assert.equal(Object.keys(caps).length, 2, 'два ядра');
const next = rewriteCoreCaps(source, { [first]: 12000 });
assert.equal(readCoreCaps(next)[first], 12000);
assert.equal(readCoreCaps(next)[second], caps[second]);
assert.equal(next.split('\n').length, source.split('\n').length, 'меняется только число');
assert.match(next, /2026-09-27, #676/, 'история решений остаётся');
assert.match(source, new RegExp(`^export const CORE_BAND = ${CORE_BAND};$`, 'm'), 'полоса теста и инструмента — одно число');
});
test('#699 потолки графов читаются и переписываются с разрядами', () => {
const source = read(BUNDLE_BUDGET_FILE);
for (const entry of BUNDLE_CEILINGS) assert.ok(readConst(source, entry.name) > 0, entry.name);
const next = rewriteConst(source, 'INITIAL_VIEW_GZIP_CEILING', 300323);
assert.equal(readConst(next, 'INITIAL_VIEW_GZIP_CEILING'), 300323);
assert.match(next, /^export const INITIAL_VIEW_GZIP_CEILING = 300_323;$/m);
assert.throws(() => readConst(source, 'NO_SUCH_CEILING'), /нет export const NO_SUCH_CEILING/);
});
test('#699 отчёт видит все три вида храповиков и называет, что делать', () => {
const rows = ratchetRows({
coreCaps: { 'a.ts': 100 }, coreFacts: { 'a.ts': 90 },
bundleCeilings: Object.fromEntries(BUNDLE_CEILINGS.map((e) => [e.name, 1000])),
bundleFacts: Object.fromEntries(BUNDLE_CEILINGS.map((e) => [e.metric, 1000])),
metrics: Object.fromEntries(METRIC_NAMES.map((n) => [n, 10])),
baseline: Object.fromEntries(METRIC_NAMES.map((n) => [n, n === 'hostRefs' ? 5 : 10])),
});
assert.equal(rows.length, 1 + BUNDLE_CEILINGS.length + METRIC_NAMES.length);
assert.equal(rows.find((r) => r.kind === 'core').state, 'loose');
assert.ok(rows.filter((r) => r.kind === 'bundle').every((r) => r.state === 'tight'));
assert.equal(rows.find((r) => r.name === 'hostRefs').state, 'over');
assert.match(formatRow(rows.find((r) => r.kind === 'core')), /факт 90 · потолок 100 \(-10, полоса \+50\) — рыхлый/);
});
test('#699 публикация беты напоминает о храповиках, но не останавливается на них', () => {
const local = read('scripts/release-prerelease.mjs');
const main = local.slice(local.indexOf('const main = async'));
assert.match(main, /'scripts\/ratchets\.mjs', 'report', '--warn'/);
assert.match(main, /'scripts\/ratchets\.mjs', 'report', '--warn'\], \{ allowFailure: true, inherit: true \}\)/);
});