mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-04 15:57:33 +01:00
Ratchet (c) ships enforcing: test/helpers/module-size.ts counts file and
top-level function lengths by brace matching over masked source (strings,
comments, regex literals and template text masked; ${} expressions kept),
covering function declarations, arrow functions assigned to consts and
route-table handler properties, with no parser dependency. Its self-test
uses template literals and code-fence braces copied from
scripts/resolvers/review.ts and design.ts. test/fixtures/module-size-ratchet.json
binds scripts/lib/shard-engine.ts (<= 800 lines, <= 150 per function) and
records the residual runner sizes (free 2352, paid 1921) as non-growth caps;
allowlist entries are keyed on file plus matched text and need a reason.
Failure output lists file:line, the rule, Fix: and the allowlist path.
touchfiles.test.ts gains the moved-code superset check over
test/fixtures/touchfile-move-goldens/ (W2 golden recorded at 96764e80:
test-strict-output.ts and test-paid-shards.ts global, test-free-shards.ts none).
105 lines
5.2 KiB
TypeScript
105 lines
5.2 KiB
TypeScript
/**
|
|
* Ratchet (c): refactor-wave owner modules stay <= 800 lines and <= 150 lines
|
|
* per top-level function; residual files may not grow past their recorded
|
|
* post-refactor size. Entries live in test/fixtures/module-size-ratchet.json;
|
|
* the counter is test/helpers/module-size.ts (brace matching, no parser).
|
|
*/
|
|
import { describe, expect, test } from 'bun:test';
|
|
import * as fs from 'node:fs';
|
|
import * as path from 'node:path';
|
|
import {
|
|
MAX_FUNCTION_LINES,
|
|
RATCHET_CONFIG,
|
|
formatRatchetFailure,
|
|
loadRatchetConfig,
|
|
maskSource,
|
|
measureFunctions,
|
|
ratchetViolations,
|
|
type RatchetConfig,
|
|
} from './helpers/module-size';
|
|
|
|
const ROOT = path.resolve(import.meta.dir, '..');
|
|
const readRepo = (file: string) => fs.readFileSync(path.join(ROOT, file), 'utf8');
|
|
|
|
/** A function of bodyLines + 3 lines. */
|
|
const fn = (name: string, bodyLines: number) =>
|
|
[`export function ${name}(value: number): number {`, ...Array.from({ length: bodyLines }, (_, i) => ` value += ${i};`), ' return value;', '}'].join('\n');
|
|
const withFiles = (files: Record<string, string>) => (file: string) => {
|
|
if (!(file in files)) throw new Error(`unexpected read: ${file}`);
|
|
return files[file];
|
|
};
|
|
|
|
describe('ratchet (c): module and function size', () => {
|
|
test('refactor-wave modules and residual files hold their limits', () => {
|
|
const config = loadRatchetConfig(ROOT);
|
|
expect(config.newModules.length).toBeGreaterThan(0);
|
|
const violations = ratchetViolations(config, readRepo);
|
|
if (violations.length > 0) throw new Error(formatRatchetFailure(violations));
|
|
});
|
|
|
|
test('counter self-test: resolver template literals with ${} and code-fence braces', () => {
|
|
const source = readRepo('test/fixtures/module-size/resolver-snippets.ts.txt');
|
|
expect(measureFunctions(source).map(({ name, line, lines }) => ({ name, line, lines }))).toEqual([
|
|
{ name: 'generateDashboardLike', line: 6, lines: 17 },
|
|
{ name: 'generateDesignSketchLike', line: 24, lines: 11 },
|
|
{ name: 'arrowWithRegex', line: 36, lines: 2 },
|
|
{ name: 'handler', line: 40, lines: 4 },
|
|
]);
|
|
const masked = maskSource(source);
|
|
expect(masked.length).toBe(source.length);
|
|
expect(masked.split('\n').length).toBe(source.split('\n').length);
|
|
// Prose braces vanish; `${}` expression code stays measurable.
|
|
expect(masked).not.toContain('{row and suffix}');
|
|
expect(masked).not.toContain('unbalanced');
|
|
expect(masked).toContain('toShellPath(ctx.paths.binDir)');
|
|
});
|
|
|
|
test('a planted 151-line function fails with file:line, the Fix and the allowlist path', () => {
|
|
const config: RatchetConfig = { newModules: [{ file: 'planted.ts', movedFrom: 'test' }], residualFiles: [], allowlist: [] };
|
|
const planted = `// header\n${fn('small', 3)}\n\n${fn('tooLong', MAX_FUNCTION_LINES - 2)}\n`;
|
|
const violations = ratchetViolations(config, withFiles({ 'planted.ts': planted }));
|
|
expect(violations).toHaveLength(1);
|
|
expect(violations[0]).toStartWith('planted.ts:9 export function tooLong(value: number): number {');
|
|
expect(violations[0]).toContain('151 lines > 150');
|
|
const message = formatRatchetFailure(violations);
|
|
expect(message).toContain('planted.ts:9');
|
|
expect(message).toContain('Fix: split the module or function');
|
|
expect(message).toContain(`Allowlist: ${RATCHET_CONFIG}`);
|
|
expect(ratchetViolations(config, withFiles({ 'planted.ts': fn('atLimit', MAX_FUNCTION_LINES - 3) }))).toEqual([]);
|
|
});
|
|
|
|
test('oversized modules and grown residual files fail', () => {
|
|
const config: RatchetConfig = {
|
|
newModules: [{ file: 'big.ts', movedFrom: 'test' }],
|
|
residualFiles: [{ file: 'residual.ts', maxLines: 3, reason: 'test' }],
|
|
allowlist: [],
|
|
};
|
|
const violations = ratchetViolations(config, withFiles({
|
|
'big.ts': `${'// line\n'.repeat(801)}`,
|
|
'residual.ts': 'a\nb\nc\nd\n',
|
|
}));
|
|
expect(violations).toEqual([
|
|
'big.ts:801 module has 801 lines > 800',
|
|
'residual.ts:4 residual file grew to 4 lines > recorded 3',
|
|
]);
|
|
});
|
|
|
|
test('allowlist entries need a reason and are keyed on file plus matched text, not line numbers', () => {
|
|
const long = fn('generatedTable', MAX_FUNCTION_LINES + 10);
|
|
const entry = { file: 'table.ts', match: 'function generatedTable(' };
|
|
const unreasoned: RatchetConfig = { newModules: [{ file: 'table.ts', movedFrom: 'test' }], residualFiles: [], allowlist: [entry] };
|
|
const unreasonedViolations = ratchetViolations(unreasoned, withFiles({ 'table.ts': long }));
|
|
expect(unreasonedViolations.some(v => v.includes('allowlist entry without a reason'))).toBe(true);
|
|
expect(unreasonedViolations.some(v => v.includes('generatedTable'))).toBe(true);
|
|
|
|
const allowed: RatchetConfig = { ...unreasoned, allowlist: [{ ...entry, reason: 'data table, not logic' }] };
|
|
expect(ratchetViolations(allowed, withFiles({ 'table.ts': long }))).toEqual([]);
|
|
expect(ratchetViolations(allowed, withFiles({ 'table.ts': `// inserted above the match\n\n${long}` }))).toEqual([]);
|
|
});
|
|
|
|
test('every committed allowlist entry and residual record carries a reason', () => {
|
|
const config = loadRatchetConfig(ROOT);
|
|
for (const entry of [...config.allowlist, ...config.residualFiles]) expect(entry.reason?.trim()).toBeTruthy();
|
|
});
|
|
});
|