71 lines
2.4 KiB
JavaScript
71 lines
2.4 KiB
JavaScript
// STATIC GUARD: no `.sort(` introduced in shared/turn.js outside the ONE
|
|||
|
|
// allowed function (sortParticipantsByInitiative, used by startEncounter to
|
||
|
|
// freeze the list once). Slot-not-sort design (docs/INITIATIVE_ORDERING.md):
|
||
|
|
// mutations = insert/move, never wholesale re-sort. A stray `.sort(` after
|
||
|
|
// start destroys drag tie-break order.
|
||
|
|
//
|
||
|
|
// This test errs the moment someone adds `.sort(` anywhere but the allowlist.
|
||
|
|
// Maintenance: add a function to ALLOWED only if it runs at startEncounter
|
||
|
|
// (one-time freeze), NOT in add/update/reorder/nextTurn paths.
|
||
|
|
|
||
|
|
'use strict';
|
||
|
|
|
||
|
|
const fs = require('fs');
|
||
|
|
const path = require('path');
|
||
|
|
|
||
|
|
const SRC = fs.readFileSync(path.join(__dirname, '..', 'turn.js'), 'utf8');
|
||
|
|
|
||
|
|
// Functions permitted to call .sort(. Listed so intent is explicit + reviewed.
|
||
|
|
const ALLOWED_SORT_FUNCTIONS = new Set([
|
||
|
|
'sortParticipantsByInitiative',
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Collect top-level function bodies by brace counting.
|
||
|
|
// Matches `function NAME(` decls AND `const NAME = ... =>` arrow fns.
|
||
|
|
// Returns [{ name, body }].
|
||
|
|
function collectBody(src, fromIdx) {
|
||
|
|
let i = fromIdx;
|
||
|
|
while (i < src.length && src[i] !== '{') i++;
|
||
|
|
let depth = 0;
|
||
|
|
const start = i;
|
||
|
|
for (; i < src.length; i++) {
|
||
|
|
if (src[i] === '{') depth++;
|
||
|
|
else if (src[i] === '}') { depth--; if (depth === 0) break; }
|
||
|
|
}
|
||
|
|
return src.slice(start, i + 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
function extractFunctions(src) {
|
||
|
|
const out = [];
|
||
|
|
const fnRe = /\bfunction\s+([A-Za-z0-9_$]+)\s*\(/g;
|
||
|
|
const arrowRe = /(?:const|let|var)\s+([A-Za-z0-9_$]+)\s*=\s*[^=;]*?=>/g;
|
||
|
|
let m;
|
||
|
|
while ((m = fnRe.exec(src)) !== null) {
|
||
|
|
out.push({ name: m[1], body: collectBody(src, m.index + m[0].length) });
|
||
|
|
}
|
||
|
|
while ((m = arrowRe.exec(src)) !== null) {
|
||
|
|
out.push({ name: m[1], body: collectBody(src, m.index + m[0].length) });
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('STATIC: no .sort( outside allowlist (slot-not-sort design)', () => {
|
||
|
|
test('every .sort( call lives in an allowed function', () => {
|
||
|
|
const fns = extractFunctions(SRC);
|
||
|
|
const offenders = [];
|
||
|
|
for (const { name, body } of fns) {
|
||
|
|
if (!ALLOWED_SORT_FUNCTIONS.has(name) && /\.sort\(/.test(body)) {
|
||
|
|
offenders.push(name);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
expect(offenders).toEqual([]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('allowed sort function is declared (no silent allowlist drift)', () => {
|
||
|
|
const declared = new Set(extractFunctions(SRC).map(f => f.name));
|
||
|
|
for (const name of ALLOWED_SORT_FUNCTIONS) {
|
||
|
|
expect(declared.has(name)).toBe(true);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|